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,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module ElasticsearchInstrumentation
6
+ @installed = false
7
+
8
+ class << self
9
+ def install!
10
+ return if @installed
11
+
12
+ installed_any = false
13
+
14
+ # Elasticsearch gem (elasticsearch-ruby)
15
+ if defined?(::Elasticsearch::Transport::Client)
16
+ install_elasticsearch_transport!
17
+ installed_any = true
18
+ end
19
+
20
+ # OpenSearch gem
21
+ if defined?(::OpenSearch::Client)
22
+ install_opensearch!
23
+ installed_any = true
24
+ end
25
+
26
+ # Elasticsearch 8.x with new client
27
+ if defined?(::Elastic::Transport::Client)
28
+ install_elastic_transport!
29
+ installed_any = true
30
+ end
31
+
32
+ return unless installed_any
33
+
34
+ @installed = true
35
+ BrainzLab.debug_log('Elasticsearch instrumentation installed')
36
+ end
37
+
38
+ def installed?
39
+ @installed
40
+ end
41
+
42
+ def reset!
43
+ @installed = false
44
+ end
45
+
46
+ private
47
+
48
+ def install_elasticsearch_transport!
49
+ ::Elasticsearch::Transport::Client.prepend(ClientPatch)
50
+ end
51
+
52
+ def install_opensearch!
53
+ ::OpenSearch::Client.prepend(ClientPatch)
54
+ end
55
+
56
+ def install_elastic_transport!
57
+ ::Elastic::Transport::Client.prepend(ClientPatch)
58
+ end
59
+ end
60
+
61
+ # Patch for Elasticsearch/OpenSearch clients
62
+ module ClientPatch
63
+ def perform_request(method, path, params = {}, body = nil, headers = nil)
64
+ return super unless should_track?
65
+
66
+ started_at = Time.now.utc
67
+
68
+ begin
69
+ response = super
70
+ record_request(method, path, params, started_at, response.status)
71
+ response
72
+ rescue StandardError => e
73
+ record_request(method, path, params, started_at, nil, e)
74
+ raise
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def should_track?
81
+ BrainzLab.configuration.instrument_elasticsearch
82
+ end
83
+
84
+ def record_request(method, path, _params, started_at, status, error = nil)
85
+ duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
86
+ operation = extract_operation(method, path)
87
+ index = extract_index(path)
88
+ level = error || (status && status >= 400) ? :error : :info
89
+
90
+ # Add breadcrumb for Reflex
91
+ if BrainzLab.configuration.reflex_enabled
92
+ BrainzLab::Reflex.add_breadcrumb(
93
+ "ES #{operation}",
94
+ category: 'elasticsearch',
95
+ level: level,
96
+ data: {
97
+ method: method.to_s.upcase,
98
+ path: truncate_path(path),
99
+ index: index,
100
+ status: status,
101
+ duration_ms: duration_ms,
102
+ error: error&.class&.name
103
+ }.compact
104
+ )
105
+ end
106
+
107
+ # Record span for Pulse
108
+ record_span(
109
+ operation: operation,
110
+ method: method,
111
+ path: path,
112
+ index: index,
113
+ started_at: started_at,
114
+ duration_ms: duration_ms,
115
+ status: status,
116
+ error: error
117
+ )
118
+
119
+ # Log to Recall
120
+ if BrainzLab.configuration.recall_enabled
121
+ log_method = error ? :warn : :debug
122
+ BrainzLab::Recall.send(
123
+ log_method,
124
+ "ES #{method.to_s.upcase} #{path} -> #{status || 'ERROR'} (#{duration_ms}ms)",
125
+ method: method.to_s.upcase,
126
+ path: path,
127
+ index: index,
128
+ status: status,
129
+ duration_ms: duration_ms,
130
+ error: error&.message
131
+ )
132
+ end
133
+ rescue StandardError => e
134
+ BrainzLab.debug_log("Elasticsearch recording failed: #{e.message}")
135
+ end
136
+
137
+ def record_span(operation:, method:, path:, index:, started_at:, duration_ms:, status:, error:)
138
+ spans = Thread.current[:brainzlab_pulse_spans]
139
+ return unless spans
140
+
141
+ span = {
142
+ span_id: SecureRandom.uuid,
143
+ name: "ES #{operation}",
144
+ kind: 'elasticsearch',
145
+ started_at: started_at,
146
+ ended_at: Time.now.utc,
147
+ duration_ms: duration_ms,
148
+ data: {
149
+ method: method.to_s.upcase,
150
+ path: truncate_path(path),
151
+ index: index,
152
+ status: status
153
+ }.compact
154
+ }
155
+
156
+ if error
157
+ span[:error] = true
158
+ span[:error_class] = error.class.name
159
+ span[:error_message] = error.message&.slice(0, 500)
160
+ end
161
+
162
+ spans << span
163
+ end
164
+
165
+ def extract_operation(method, path)
166
+ method_str = method.to_s.upcase
167
+
168
+ case path
169
+ when %r{/_search} then 'search'
170
+ when %r{/_bulk} then 'bulk'
171
+ when %r{/_count} then 'count'
172
+ when %r{/_mget} then 'mget'
173
+ when %r{/_msearch} then 'msearch'
174
+ when %r{/_update_by_query} then 'update_by_query'
175
+ when %r{/_delete_by_query} then 'delete_by_query'
176
+ when %r{/_refresh} then 'refresh'
177
+ when %r{/_mapping} then 'mapping'
178
+ when %r{/_settings} then 'settings'
179
+ when %r{/_alias} then 'alias'
180
+ when %r{/_analyze} then 'analyze'
181
+ else
182
+ case method_str
183
+ when 'GET' then 'get'
184
+ when 'POST' then 'index'
185
+ when 'PUT' then 'update'
186
+ when 'DELETE' then 'delete'
187
+ when 'HEAD' then 'exists'
188
+ else method_str.downcase
189
+ end
190
+ end
191
+ end
192
+
193
+ def extract_index(path)
194
+ # Extract index name from path like /my-index/_search
195
+ match = path.match(%r{^/([^/_]+)})
196
+ match[1] if match && !match[1].start_with?('_')
197
+ rescue StandardError
198
+ nil
199
+ end
200
+
201
+ def truncate_path(path)
202
+ return nil unless path
203
+
204
+ path.to_s[0, 200]
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module ExconInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::Excon)
9
+
10
+ install_middleware!
11
+
12
+ BrainzLab.debug_log('[Instrumentation] Excon instrumentation installed')
13
+ end
14
+
15
+ private
16
+
17
+ def install_middleware!
18
+ # Add our instrumentor to Excon defaults
19
+ ::Excon.defaults[:instrumentor] = BrainzLabInstrumentor
20
+
21
+ # Also set up middleware
22
+ return unless ::Excon.defaults[:middlewares]
23
+
24
+ ::Excon.defaults[:middlewares] = [Middleware] + ::Excon.defaults[:middlewares]
25
+ end
26
+ end
27
+
28
+ # Excon Instrumentor for ActiveSupport-style notifications
29
+ module BrainzLabInstrumentor
30
+ def self.instrument(name, params = {})
31
+ started_at = Time.now
32
+
33
+ begin
34
+ result = yield if block_given?
35
+ track_request(name, params, started_at, nil)
36
+ result
37
+ rescue StandardError => e
38
+ track_request(name, params, started_at, e)
39
+ raise
40
+ end
41
+ end
42
+
43
+ def self.track_request(_name, params, started_at, error)
44
+ return if skip_tracking?(params)
45
+
46
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
47
+ host = params[:host] || 'unknown'
48
+ method = (params[:method] || 'GET').to_s.upcase
49
+ path = params[:path] || '/'
50
+ status = params[:status]
51
+
52
+ # Add breadcrumb
53
+ BrainzLab::Reflex.add_breadcrumb(
54
+ "HTTP #{method} #{host}#{path}",
55
+ category: 'http',
56
+ level: error ? :error : :info,
57
+ data: {
58
+ method: method,
59
+ host: host,
60
+ path: path,
61
+ status: status,
62
+ duration_ms: duration_ms
63
+ }
64
+ )
65
+
66
+ # Track with Pulse
67
+ if BrainzLab.configuration.pulse_effectively_enabled?
68
+ BrainzLab::Pulse.span('http.excon', kind: 'http') do
69
+ # Already completed, just recording
70
+ end
71
+ end
72
+
73
+ # Track with Flux
74
+ return unless BrainzLab.configuration.flux_effectively_enabled?
75
+
76
+ tags = { host: host, method: method, status: status.to_s }
77
+ BrainzLab::Flux.distribution('http.excon.duration_ms', duration_ms, tags: tags)
78
+ BrainzLab::Flux.increment('http.excon.requests', tags: tags)
79
+
80
+ return unless error || (status && status >= 400)
81
+
82
+ BrainzLab::Flux.increment('http.excon.errors', tags: tags)
83
+ end
84
+
85
+ def self.skip_tracking?(params)
86
+ host = params[:host]
87
+ return true unless host
88
+
89
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
90
+ ignore_hosts.any? { |h| host.include?(h) }
91
+ end
92
+ end
93
+
94
+ # Excon Middleware
95
+ class Middleware
96
+ def initialize(stack)
97
+ @stack = stack
98
+ end
99
+
100
+ def request_call(datum)
101
+ datum[:brainzlab_started_at] = Time.now
102
+ @stack.request_call(datum)
103
+ end
104
+
105
+ def response_call(datum)
106
+ track_response(datum)
107
+ @stack.response_call(datum)
108
+ end
109
+
110
+ def error_call(datum)
111
+ track_response(datum, error: true)
112
+ @stack.error_call(datum)
113
+ end
114
+
115
+ private
116
+
117
+ def track_response(datum, error: false)
118
+ started_at = datum[:brainzlab_started_at]
119
+ return unless started_at
120
+
121
+ host = datum[:host]
122
+ return if skip_host?(host)
123
+
124
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
125
+ method = (datum[:method] || 'GET').to_s.upcase
126
+ path = datum[:path] || '/'
127
+ status = datum[:response]&.dig(:status)
128
+
129
+ BrainzLab::Reflex.add_breadcrumb(
130
+ "HTTP #{method} #{host}#{path} -> #{status || 'error'}",
131
+ category: 'http',
132
+ level: error ? :error : :info,
133
+ data: { method: method, host: host, status: status, duration_ms: duration_ms }
134
+ )
135
+
136
+ return unless BrainzLab.configuration.flux_effectively_enabled?
137
+
138
+ tags = { host: host, method: method }
139
+ tags[:status] = status.to_s if status
140
+ BrainzLab::Flux.distribution('http.excon.duration_ms', duration_ms, tags: tags)
141
+ end
142
+
143
+ def skip_host?(host)
144
+ return true unless host
145
+
146
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
147
+ ignore_hosts.any? { |h| host.include?(h) }
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module FaradayMiddleware
6
+ @installed = false
7
+
8
+ class << self
9
+ def install!
10
+ return unless defined?(::Faraday)
11
+ return if @installed
12
+
13
+ # Register the middleware with Faraday
14
+ ::Faraday::Middleware.register_middleware(brainzlab: Middleware)
15
+
16
+ @installed = true
17
+ BrainzLab.debug_log('Faraday instrumentation installed')
18
+ end
19
+
20
+ def installed?
21
+ @installed
22
+ end
23
+
24
+ def reset!
25
+ @installed = false
26
+ end
27
+ end
28
+
29
+ # Faraday middleware for HTTP request instrumentation
30
+ # Usage:
31
+ # conn = Faraday.new do |f|
32
+ # f.use :brainzlab
33
+ # # or
34
+ # f.use BrainzLab::Instrumentation::FaradayMiddleware::Middleware
35
+ # end
36
+ class Middleware < ::Faraday::Middleware
37
+ def initialize(app, options = {})
38
+ super(app)
39
+ @options = options
40
+ end
41
+
42
+ def call(env)
43
+ return @app.call(env) unless should_track?(env)
44
+
45
+ # Inject distributed tracing context
46
+ inject_trace_context(env)
47
+
48
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+
50
+ begin
51
+ response = @app.call(env)
52
+ track_request(env, response.status, started_at)
53
+ response
54
+ rescue ::Faraday::Error => e
55
+ error_info = e.class.name
56
+ track_request(env, e.response&.dig(:status), started_at, error_info)
57
+ raise
58
+ rescue StandardError => e
59
+ error_info = e.class.name
60
+ track_request(env, nil, started_at, error_info)
61
+ raise
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def should_track?(env)
68
+ return false unless BrainzLab.configuration.instrument_http
69
+
70
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
71
+ host = env.url.host
72
+ !ignore_hosts.include?(host)
73
+ end
74
+
75
+ def inject_trace_context(env)
76
+ return unless BrainzLab.configuration.pulse_enabled
77
+
78
+ headers = {}
79
+ BrainzLab::Pulse.inject(headers, format: :all)
80
+
81
+ headers.each do |key, value|
82
+ env.request_headers[key] = value
83
+ end
84
+ rescue StandardError => e
85
+ BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
86
+ end
87
+
88
+ def track_request(env, status, started_at, error = nil)
89
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
90
+ method = env.method.to_s.upcase
91
+ url = sanitize_url(env.url)
92
+ host = env.url.host
93
+ path = env.url.path
94
+ level = error || (status && status >= 400) ? :error : :info
95
+
96
+ # Add breadcrumb for Reflex
97
+ if BrainzLab.configuration.reflex_enabled
98
+ BrainzLab::Reflex.add_breadcrumb(
99
+ "#{method} #{url}",
100
+ category: 'http.faraday',
101
+ level: level,
102
+ data: {
103
+ method: method,
104
+ url: url,
105
+ host: host,
106
+ path: path,
107
+ status_code: status,
108
+ duration_ms: duration_ms,
109
+ error: error
110
+ }.compact
111
+ )
112
+ end
113
+
114
+ # Record span for Pulse APM
115
+ record_pulse_span(method, host, path, status, duration_ms, error)
116
+
117
+ # Log to Recall at debug level
118
+ if BrainzLab.configuration.recall_enabled
119
+ BrainzLab::Recall.debug(
120
+ "HTTP #{method} #{url} -> #{status || 'ERROR'}",
121
+ method: method,
122
+ url: url,
123
+ host: host,
124
+ status_code: status,
125
+ duration_ms: duration_ms,
126
+ error: error
127
+ )
128
+ end
129
+ rescue StandardError => e
130
+ BrainzLab.debug_log("Faraday instrumentation error: #{e.message}")
131
+ end
132
+
133
+ def record_pulse_span(method, host, path, status, duration_ms, error)
134
+ spans = Thread.current[:brainzlab_pulse_spans]
135
+ return unless spans
136
+
137
+ span = {
138
+ span_id: SecureRandom.uuid,
139
+ name: "HTTP #{method} #{host}",
140
+ kind: 'http',
141
+ started_at: Time.now.utc - (duration_ms / 1000.0),
142
+ ended_at: Time.now.utc,
143
+ duration_ms: duration_ms,
144
+ data: {
145
+ method: method,
146
+ host: host,
147
+ path: path,
148
+ status: status
149
+ }.compact
150
+ }
151
+
152
+ if error
153
+ span[:error] = true
154
+ span[:error_class] = error
155
+ end
156
+
157
+ spans << span
158
+ end
159
+
160
+ def sanitize_url(url)
161
+ # Remove sensitive query parameters
162
+ uri = url.dup
163
+ if uri.query
164
+ params = URI.decode_www_form(uri.query).reject do |key, _|
165
+ sensitive_param?(key)
166
+ end
167
+ uri.query = params.empty? ? nil : URI.encode_www_form(params)
168
+ end
169
+ uri.to_s
170
+ rescue StandardError
171
+ url.to_s
172
+ end
173
+
174
+ def sensitive_param?(key)
175
+ key = key.to_s.downcase
176
+ %w[token api_key apikey secret password auth key].any? { |s| key.include?(s) }
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module GoodJobInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::GoodJob)
9
+
10
+ install_notifier!
11
+ install_middleware!
12
+
13
+ BrainzLab.debug_log('[Instrumentation] GoodJob instrumentation installed')
14
+ end
15
+
16
+ private
17
+
18
+ def install_notifier!
19
+ return unless defined?(::ActiveSupport::Notifications)
20
+
21
+ # GoodJob emits ActiveSupport notifications
22
+ ::ActiveSupport::Notifications.subscribe('perform_job.good_job') do |*args|
23
+ event = ::ActiveSupport::Notifications::Event.new(*args)
24
+ handle_perform(event)
25
+ end
26
+
27
+ ::ActiveSupport::Notifications.subscribe('finished_job_task.good_job') do |*args|
28
+ event = ::ActiveSupport::Notifications::Event.new(*args)
29
+ handle_finished(event)
30
+ end
31
+ end
32
+
33
+ def install_middleware!
34
+ return unless defined?(::GoodJob::Adapter)
35
+
36
+ # Add our callback to GoodJob
37
+ return unless ::GoodJob.respond_to?(:on_thread_error)
38
+
39
+ ::GoodJob.on_thread_error = lambda { |error|
40
+ if BrainzLab.configuration.reflex_effectively_enabled?
41
+ BrainzLab::Reflex.capture(error,
42
+ tags: { source: 'good_job' })
43
+ end
44
+ }
45
+ end
46
+
47
+ def handle_perform(event)
48
+ payload = event.payload
49
+ job = payload[:job]
50
+ job_class = job&.class&.name || payload[:job_class] || 'Unknown'
51
+ queue = job&.queue_name || payload[:queue_name] || 'default'
52
+ duration_ms = event.duration.round(2)
53
+
54
+ # Track with Pulse
55
+ if BrainzLab.configuration.pulse_effectively_enabled?
56
+ BrainzLab::Pulse.record_trace(
57
+ "job.#{job_class}",
58
+ kind: 'job',
59
+ started_at: event.time,
60
+ ended_at: event.end,
61
+ job_class: job_class,
62
+ job_id: job&.job_id || payload[:job_id],
63
+ queue: queue,
64
+ error: payload[:error].present?,
65
+ error_class: payload[:error]&.class&.name,
66
+ error_message: payload[:error]&.message
67
+ )
68
+ end
69
+
70
+ # Track with Flux
71
+ if BrainzLab.configuration.flux_effectively_enabled?
72
+ tags = { job_class: job_class, queue: queue }
73
+ BrainzLab::Flux.distribution('good_job.job.duration_ms', duration_ms, tags: tags)
74
+ BrainzLab::Flux.increment('good_job.job.processed', tags: tags)
75
+
76
+ BrainzLab::Flux.increment('good_job.job.failed', tags: tags) if payload[:error]
77
+ end
78
+
79
+ # Capture error with Reflex
80
+ return unless payload[:error] && BrainzLab.configuration.reflex_effectively_enabled?
81
+
82
+ BrainzLab::Reflex.capture(payload[:error],
83
+ tags: { job_class: job_class, queue: queue, source: 'good_job' },
84
+ extra: { job_id: job&.job_id, duration_ms: duration_ms })
85
+ end
86
+
87
+ def handle_finished(event)
88
+ payload = event.payload
89
+
90
+ return unless BrainzLab.configuration.flux_effectively_enabled?
91
+
92
+ result = payload[:result]
93
+ if result == :discarded
94
+ BrainzLab::Flux.increment('good_job.job.discarded')
95
+ elsif result == :retried
96
+ BrainzLab::Flux.increment('good_job.job.retried')
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end