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,193 @@
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 = {}, &)
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
+
37
+ begin
38
+ response = super
39
+ track_request(http_method, path, options, response.code, started_at)
40
+ response
41
+ rescue StandardError => e
42
+ error_info = e.class.name
43
+ track_request(http_method, path, options, nil, started_at, error_info)
44
+ raise
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def should_track?(path, options)
51
+ return false unless BrainzLab.configuration.instrument_http
52
+
53
+ uri = parse_uri(path, options)
54
+ return true unless uri
55
+
56
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
57
+ !ignore_hosts.include?(uri.host)
58
+ end
59
+
60
+ def inject_trace_context(options)
61
+ return options unless BrainzLab.configuration.pulse_enabled
62
+
63
+ options = options.dup
64
+ options[:headers] ||= {}
65
+
66
+ trace_headers = {}
67
+ BrainzLab::Pulse.inject(trace_headers, format: :all)
68
+
69
+ options[:headers] = options[:headers].merge(trace_headers)
70
+ options
71
+ rescue StandardError => e
72
+ BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
73
+ options
74
+ end
75
+
76
+ def track_request(http_method, path, options, status, started_at, error = nil)
77
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
78
+ method = extract_method_name(http_method)
79
+ uri = parse_uri(path, options)
80
+ url = uri ? sanitize_url(uri) : path.to_s
81
+ host = uri&.host || 'unknown'
82
+ request_path = uri&.path || path.to_s
83
+ level = error || (status && status >= 400) ? :error : :info
84
+
85
+ # Add breadcrumb for Reflex
86
+ if BrainzLab.configuration.reflex_enabled
87
+ BrainzLab::Reflex.add_breadcrumb(
88
+ "#{method} #{url}",
89
+ category: 'http.httparty',
90
+ level: level,
91
+ data: {
92
+ method: method,
93
+ url: url,
94
+ host: host,
95
+ path: request_path,
96
+ status_code: status,
97
+ duration_ms: duration_ms,
98
+ error: error
99
+ }.compact
100
+ )
101
+ end
102
+
103
+ # Record span for Pulse APM
104
+ record_pulse_span(method, host, request_path, status, duration_ms, error)
105
+
106
+ # Log to Recall at debug level
107
+ if BrainzLab.configuration.recall_enabled
108
+ BrainzLab::Recall.debug(
109
+ "HTTP #{method} #{url} -> #{status || 'ERROR'}",
110
+ method: method,
111
+ url: url,
112
+ host: host,
113
+ status_code: status,
114
+ duration_ms: duration_ms,
115
+ error: error
116
+ )
117
+ end
118
+ rescue StandardError => e
119
+ BrainzLab.debug_log("HTTParty instrumentation error: #{e.message}")
120
+ end
121
+
122
+ def record_pulse_span(method, host, path, status, duration_ms, error)
123
+ spans = Thread.current[:brainzlab_pulse_spans]
124
+ return unless spans
125
+
126
+ span = {
127
+ span_id: SecureRandom.uuid,
128
+ name: "HTTP #{method} #{host}",
129
+ kind: 'http',
130
+ started_at: Time.now.utc - (duration_ms / 1000.0),
131
+ ended_at: Time.now.utc,
132
+ duration_ms: duration_ms,
133
+ data: {
134
+ method: method,
135
+ host: host,
136
+ path: path,
137
+ status: status
138
+ }.compact
139
+ }
140
+
141
+ if error
142
+ span[:error] = true
143
+ span[:error_class] = error
144
+ end
145
+
146
+ spans << span
147
+ end
148
+
149
+ def extract_method_name(http_method)
150
+ case http_method.name
151
+ when /Get$/ then 'GET'
152
+ when /Post$/ then 'POST'
153
+ when /Put$/ then 'PUT'
154
+ when /Patch$/ then 'PATCH'
155
+ when /Delete$/ then 'DELETE'
156
+ when /Head$/ then 'HEAD'
157
+ when /Options$/ then 'OPTIONS'
158
+ else http_method.name.split('::').last.upcase
159
+ end
160
+ end
161
+
162
+ def parse_uri(path, options)
163
+ base_uri = options[:base_uri]
164
+ if base_uri
165
+ URI.join(base_uri.to_s, path.to_s)
166
+ else
167
+ URI.parse(path.to_s)
168
+ end
169
+ rescue URI::InvalidURIError
170
+ nil
171
+ end
172
+
173
+ def sanitize_url(uri)
174
+ result = uri.dup
175
+ if result.query
176
+ params = URI.decode_www_form(result.query).reject do |key, _|
177
+ sensitive_param?(key)
178
+ end
179
+ result.query = params.empty? ? nil : URI.encode_www_form(params)
180
+ end
181
+ result.to_s
182
+ rescue StandardError
183
+ uri.to_s
184
+ end
185
+
186
+ def sensitive_param?(key)
187
+ key = key.to_s.downcase
188
+ %w[token api_key apikey secret password auth key].any? { |s| key.include?(s) }
189
+ end
190
+ end
191
+ end
192
+ end
193
+ 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
+ return unless ::Mongoid.respond_to?(:subscribe)
55
+
56
+ ::Mongoid.subscribe(CommandSubscriber.new)
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
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module NetHttp
6
+ @installed = false
7
+
8
+ class << self
9
+ def install!
10
+ return if @installed
11
+
12
+ ::Net::HTTP.prepend(Patch)
13
+ @installed = true
14
+ end
15
+
16
+ def installed?
17
+ @installed
18
+ end
19
+
20
+ # For testing purposes
21
+ def reset!
22
+ @installed = false
23
+ end
24
+ end
25
+
26
+ module Patch
27
+ def request(req, body = nil, &)
28
+ return super unless should_track?
29
+
30
+ # Inject distributed tracing context into outgoing request headers
31
+ inject_trace_context(req)
32
+
33
+ url = build_url(req)
34
+ method = req.method
35
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+
37
+ begin
38
+ response = super
39
+ track_request(method, url, response.code.to_i, started_at)
40
+ response
41
+ rescue StandardError => e
42
+ track_request(method, url, nil, started_at, e.class.name)
43
+ raise
44
+ end
45
+ end
46
+
47
+ def inject_trace_context(req)
48
+ return unless BrainzLab.configuration.pulse_enabled
49
+
50
+ # Build headers hash and inject trace context
51
+ headers = {}
52
+ BrainzLab::Pulse.inject(headers, format: :all)
53
+
54
+ # Apply headers to request
55
+ headers.each do |key, value|
56
+ req[key] = value
57
+ end
58
+ rescue StandardError => e
59
+ BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
60
+ end
61
+
62
+ private
63
+
64
+ def should_track?
65
+ return false unless BrainzLab.configuration.instrument_http
66
+ # Skip tracking SDK's own HTTP calls to its service endpoints
67
+ # to prevent recursive cascading (SDK HTTP → track → Recall.debug → buffer → flush → SDK HTTP → ...)
68
+ return false if BrainzLab.configuration.sdk_service_hosts.include?(address)
69
+
70
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
71
+ !ignore_hosts.include?(address)
72
+ end
73
+
74
+ def build_url(req)
75
+ scheme = use_ssl? ? 'https' : 'http'
76
+ port_str = if (use_ssl? && port == 443) || (!use_ssl? && port == 80)
77
+ ''
78
+ else
79
+ ":#{port}"
80
+ end
81
+ "#{scheme}://#{address}#{port_str}#{req.path}"
82
+ end
83
+
84
+ def track_request(method, url, status, started_at, error = nil)
85
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
86
+ level = error || (status && status >= 400) ? :error : :info
87
+
88
+ BrainzLab.with_instrumentation_guard do
89
+ # Add breadcrumb for Reflex (in-memory, safe)
90
+ if BrainzLab.configuration.reflex_enabled
91
+ BrainzLab::Reflex.add_breadcrumb(
92
+ "#{method} #{url}",
93
+ category: 'http',
94
+ level: level,
95
+ data: { method: method, url: url, status_code: status, duration_ms: duration_ms, error: error }.compact
96
+ )
97
+ end
98
+
99
+ # Log to Recall at debug level (skipped if already instrumenting)
100
+ if BrainzLab.configuration.recall_enabled
101
+ BrainzLab::Recall.debug(
102
+ "HTTP #{method} #{url} -> #{status || 'ERROR'}",
103
+ method: method, url: url, status_code: status, duration_ms: duration_ms, error: error
104
+ )
105
+ end
106
+ end
107
+ rescue StandardError => e
108
+ # Don't let instrumentation errors crash the app
109
+ BrainzLab.configuration.logger&.error("[BrainzLab] HTTP instrumentation error: #{e.message}")
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ class RailsDeprecation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::Rails)
9
+ return if @installed
10
+
11
+ install_deprecation_subscriber!
12
+
13
+ @installed = true
14
+ BrainzLab.debug_log('Rails deprecation instrumentation installed')
15
+ end
16
+
17
+ def installed?
18
+ @installed == true
19
+ end
20
+
21
+ private
22
+
23
+ # ============================================
24
+ # deprecation.rails
25
+ # Fired when a deprecated Rails API is used
26
+ # ============================================
27
+ def install_deprecation_subscriber!
28
+ ActiveSupport::Notifications.subscribe('deprecation.rails') do |*args|
29
+ event = ActiveSupport::Notifications::Event.new(*args)
30
+ handle_deprecation(event)
31
+ end
32
+ end
33
+
34
+ def handle_deprecation(event)
35
+ payload = event.payload
36
+
37
+ message = payload[:message]
38
+ callstack = payload[:callstack]
39
+ gem_name = payload[:gem_name]
40
+ deprecation_horizon = payload[:deprecation_horizon]
41
+
42
+ # Extract relevant caller info
43
+ caller_info = extract_caller_info(callstack)
44
+
45
+ # Record breadcrumb with warning level
46
+ if BrainzLab.configuration.reflex_effectively_enabled?
47
+ BrainzLab::Reflex.add_breadcrumb(
48
+ "Deprecation: #{truncate_message(message)}",
49
+ category: 'rails.deprecation',
50
+ level: :warning,
51
+ data: {
52
+ message: truncate_message(message, 500),
53
+ gem_name: gem_name,
54
+ deprecation_horizon: deprecation_horizon,
55
+ caller: caller_info
56
+ }.compact
57
+ )
58
+ end
59
+
60
+ # Log to Recall for tracking
61
+ if BrainzLab.configuration.recall_effectively_enabled?
62
+ BrainzLab::Recall.warn(
63
+ "Rails deprecation warning",
64
+ message: truncate_message(message, 500),
65
+ gem_name: gem_name,
66
+ deprecation_horizon: deprecation_horizon,
67
+ caller: caller_info,
68
+ callstack: truncate_callstack(callstack)
69
+ )
70
+ end
71
+
72
+ # Track deprecation count in Pulse metrics
73
+ record_deprecation_metric(gem_name, deprecation_horizon)
74
+ rescue StandardError => e
75
+ BrainzLab.debug_log("Rails deprecation instrumentation failed: #{e.message}")
76
+ end
77
+
78
+ # ============================================
79
+ # Helper Methods
80
+ # ============================================
81
+ def extract_caller_info(callstack)
82
+ return nil unless callstack.is_a?(Array) && callstack.any?
83
+
84
+ # Find the first non-Rails, non-gem caller
85
+ app_caller = callstack.find do |line|
86
+ line_str = line.to_s
87
+ !line_str.include?('/gems/') &&
88
+ !line_str.include?('/ruby/') &&
89
+ !line_str.include?('/bundler/')
90
+ end
91
+
92
+ (app_caller || callstack.first).to_s
93
+ end
94
+
95
+ def truncate_callstack(callstack, max_lines = 5)
96
+ return nil unless callstack.is_a?(Array)
97
+
98
+ callstack.first(max_lines).map(&:to_s)
99
+ end
100
+
101
+ def truncate_message(message, max_length = 200)
102
+ return 'unknown' unless message
103
+
104
+ msg_str = message.to_s
105
+ if msg_str.length > max_length
106
+ "#{msg_str[0, max_length - 3]}..."
107
+ else
108
+ msg_str
109
+ end
110
+ end
111
+
112
+ def record_deprecation_metric(gem_name, horizon)
113
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
114
+
115
+ # If Pulse has a counter/metric API, use it here
116
+ # For now, we just add a span to track it
117
+ tracer = BrainzLab::Pulse.tracer
118
+ return unless tracer.current_trace
119
+
120
+ span_data = {
121
+ span_id: SecureRandom.uuid,
122
+ name: 'rails.deprecation',
123
+ kind: 'internal',
124
+ started_at: Time.now,
125
+ ended_at: Time.now,
126
+ duration_ms: 0,
127
+ error: false,
128
+ data: {
129
+ 'deprecation.gem_name' => gem_name,
130
+ 'deprecation.horizon' => horizon
131
+ }.compact
132
+ }
133
+
134
+ tracer.current_spans << span_data
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end