brainzlab 0.1.1 → 0.1.3

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +6 -21
  3. data/README.md +24 -2
  4. data/lib/brainzlab/beacon/client.rb +207 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +372 -32
  8. data/lib/brainzlab/context.rb +2 -3
  9. data/lib/brainzlab/cortex/cache.rb +59 -0
  10. data/lib/brainzlab/cortex/client.rb +139 -0
  11. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  12. data/lib/brainzlab/cortex.rb +223 -0
  13. data/lib/brainzlab/dendrite/client.rb +230 -0
  14. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  15. data/lib/brainzlab/dendrite.rb +195 -0
  16. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  17. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  18. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  19. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  20. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  21. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  22. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  23. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  24. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  25. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  26. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  27. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  28. data/lib/brainzlab/devtools.rb +75 -0
  29. data/lib/brainzlab/flux/buffer.rb +96 -0
  30. data/lib/brainzlab/flux/client.rb +68 -0
  31. data/lib/brainzlab/flux/provisioner.rb +57 -0
  32. data/lib/brainzlab/flux.rb +174 -0
  33. data/lib/brainzlab/instrumentation/action_mailer.rb +14 -13
  34. data/lib/brainzlab/instrumentation/active_record.rb +28 -13
  35. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  36. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  37. data/lib/brainzlab/instrumentation/delayed_job.rb +27 -29
  38. data/lib/brainzlab/instrumentation/elasticsearch.rb +23 -24
  39. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  40. data/lib/brainzlab/instrumentation/faraday.rb +3 -4
  41. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  42. data/lib/brainzlab/instrumentation/grape.rb +24 -24
  43. data/lib/brainzlab/instrumentation/graphql.rb +24 -23
  44. data/lib/brainzlab/instrumentation/httparty.rb +13 -14
  45. data/lib/brainzlab/instrumentation/mongodb.rb +7 -7
  46. data/lib/brainzlab/instrumentation/net_http.rb +6 -6
  47. data/lib/brainzlab/instrumentation/redis.rb +14 -21
  48. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  49. data/lib/brainzlab/instrumentation/sidekiq.rb +29 -28
  50. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  51. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  52. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  53. data/lib/brainzlab/instrumentation.rb +84 -12
  54. data/lib/brainzlab/nerve/client.rb +215 -0
  55. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  56. data/lib/brainzlab/nerve.rb +219 -0
  57. data/lib/brainzlab/pulse/client.rb +15 -11
  58. data/lib/brainzlab/pulse/instrumentation.rb +90 -53
  59. data/lib/brainzlab/pulse/propagation.rb +29 -29
  60. data/lib/brainzlab/pulse/provisioner.rb +12 -12
  61. data/lib/brainzlab/pulse/tracer.rb +4 -4
  62. data/lib/brainzlab/pulse.rb +14 -14
  63. data/lib/brainzlab/rails/log_formatter.rb +127 -121
  64. data/lib/brainzlab/rails/log_subscriber.rb +70 -77
  65. data/lib/brainzlab/rails/railtie.rb +96 -86
  66. data/lib/brainzlab/recall/buffer.rb +1 -1
  67. data/lib/brainzlab/recall/client.rb +14 -10
  68. data/lib/brainzlab/recall/logger.rb +16 -18
  69. data/lib/brainzlab/recall/provisioner.rb +29 -12
  70. data/lib/brainzlab/recall.rb +14 -11
  71. data/lib/brainzlab/reflex/breadcrumbs.rb +2 -2
  72. data/lib/brainzlab/reflex/client.rb +14 -10
  73. data/lib/brainzlab/reflex/provisioner.rb +12 -12
  74. data/lib/brainzlab/reflex.rb +31 -31
  75. data/lib/brainzlab/sentinel/client.rb +216 -0
  76. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  77. data/lib/brainzlab/sentinel.rb +165 -0
  78. data/lib/brainzlab/signal/client.rb +60 -0
  79. data/lib/brainzlab/signal/provisioner.rb +55 -0
  80. data/lib/brainzlab/signal.rb +136 -0
  81. data/lib/brainzlab/synapse/client.rb +288 -0
  82. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  83. data/lib/brainzlab/synapse.rb +270 -0
  84. data/lib/brainzlab/utilities/circuit_breaker.rb +261 -0
  85. data/lib/brainzlab/utilities/health_check.rb +294 -0
  86. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  87. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  88. data/lib/brainzlab/utilities.rb +17 -0
  89. data/lib/brainzlab/vault/cache.rb +80 -0
  90. data/lib/brainzlab/vault/client.rb +196 -0
  91. data/lib/brainzlab/vault/provisioner.rb +49 -0
  92. data/lib/brainzlab/vault.rb +262 -0
  93. data/lib/brainzlab/version.rb +1 -1
  94. data/lib/brainzlab/vision/client.rb +128 -0
  95. data/lib/brainzlab/vision/provisioner.rb +136 -0
  96. data/lib/brainzlab/vision.rb +155 -0
  97. data/lib/brainzlab-sdk.rb +1 -1
  98. data/lib/brainzlab.rb +112 -13
  99. data/lib/generators/brainzlab/install/install_generator.rb +29 -27
  100. metadata +60 -1
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module AWSInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::Aws)
9
+
10
+ install_plugin!
11
+
12
+ BrainzLab.debug_log('[Instrumentation] AWS SDK instrumentation installed')
13
+ end
14
+
15
+ private
16
+
17
+ def install_plugin!
18
+ # AWS SDK v3 uses plugins
19
+ if defined?(::Aws::Plugins)
20
+ ::Aws.config[:plugins] ||= []
21
+ ::Aws.config[:plugins] << BrainzLabPlugin unless ::Aws.config[:plugins].include?(BrainzLabPlugin)
22
+ end
23
+
24
+ # Also hook into Seahorse for lower-level tracking
25
+ return unless defined?(::Seahorse::Client::Base)
26
+
27
+ install_seahorse_handler!
28
+ end
29
+
30
+ def install_seahorse_handler!
31
+ handler_class = Class.new(::Seahorse::Client::Handler) do
32
+ def call(context)
33
+ started_at = Time.now
34
+ service = context.client.class.name.split('::')[1] || 'AWS'
35
+ operation = context.operation_name.to_s
36
+
37
+ begin
38
+ response = @handler.call(context)
39
+ track_success(service, operation, started_at, context, response)
40
+ response
41
+ rescue StandardError => e
42
+ track_error(service, operation, started_at, context, e)
43
+ raise
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def track_success(service, operation, started_at, context, _response)
50
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
51
+
52
+ BrainzLab::Reflex.add_breadcrumb(
53
+ "AWS #{service}.#{operation}",
54
+ category: 'aws',
55
+ level: :info,
56
+ data: {
57
+ service: service,
58
+ operation: operation,
59
+ region: context.config.region,
60
+ duration_ms: duration_ms
61
+ }
62
+ )
63
+
64
+ return unless BrainzLab.configuration.flux_effectively_enabled?
65
+
66
+ tags = { service: service, operation: operation, region: context.config.region }
67
+ BrainzLab::Flux.distribution('aws.duration_ms', duration_ms, tags: tags)
68
+ BrainzLab::Flux.increment('aws.requests', tags: tags)
69
+ end
70
+
71
+ def track_error(service, operation, started_at, _context, error)
72
+ ((Time.now - started_at) * 1000).round(2)
73
+
74
+ BrainzLab::Reflex.add_breadcrumb(
75
+ "AWS #{service}.#{operation} failed: #{error.message}",
76
+ category: 'aws',
77
+ level: :error,
78
+ data: {
79
+ service: service,
80
+ operation: operation,
81
+ error: error.class.name
82
+ }
83
+ )
84
+
85
+ return unless BrainzLab.configuration.flux_effectively_enabled?
86
+
87
+ tags = { service: service, operation: operation, error_class: error.class.name }
88
+ BrainzLab::Flux.increment('aws.errors', tags: tags)
89
+ end
90
+ end
91
+
92
+ ::Seahorse::Client::Base.add_plugin(
93
+ Class.new(::Seahorse::Client::Plugin) do
94
+ define_method(:add_handlers) do |handlers, _config|
95
+ handlers.add(handler_class, step: :validate, priority: 0)
96
+ end
97
+ end
98
+ )
99
+ end
100
+ end
101
+
102
+ # Aws SDK Plugin
103
+ class BrainzLabPlugin
104
+ def self.add_handlers(handlers, _config)
105
+ handlers.add(Handler, step: :validate, priority: 0)
106
+ end
107
+
108
+ class Handler
109
+ def initialize(handler)
110
+ @handler = handler
111
+ end
112
+
113
+ def call(context)
114
+ started_at = Time.now
115
+ service = extract_service(context)
116
+ operation = context.operation_name.to_s
117
+
118
+ begin
119
+ response = @handler.call(context)
120
+ track_request(service, operation, started_at, context, response)
121
+ response
122
+ rescue StandardError => e
123
+ track_error(service, operation, started_at, context, e)
124
+ raise
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def extract_service(context)
131
+ context.client.class.name.to_s.split('::')[1] || 'AWS'
132
+ end
133
+
134
+ def track_request(service, operation, started_at, context, _response)
135
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
136
+ region = begin
137
+ context.config.region
138
+ rescue StandardError
139
+ 'unknown'
140
+ end
141
+
142
+ BrainzLab::Reflex.add_breadcrumb(
143
+ "AWS #{service}.#{operation}",
144
+ category: 'aws',
145
+ level: :info,
146
+ data: {
147
+ service: service,
148
+ operation: operation,
149
+ region: region,
150
+ duration_ms: duration_ms,
151
+ retries: context.retries
152
+ }
153
+ )
154
+
155
+ return unless BrainzLab.configuration.flux_effectively_enabled?
156
+
157
+ tags = { service: service, operation: operation, region: region }
158
+ BrainzLab::Flux.distribution('aws.duration_ms', duration_ms, tags: tags)
159
+ BrainzLab::Flux.increment('aws.requests', tags: tags)
160
+
161
+ return unless context.retries.positive?
162
+
163
+ BrainzLab::Flux.increment('aws.retries', value: context.retries, tags: tags)
164
+ end
165
+
166
+ def track_error(service, operation, _started_at, _context, error)
167
+ BrainzLab::Reflex.add_breadcrumb(
168
+ "AWS #{service}.#{operation} failed",
169
+ category: 'aws',
170
+ level: :error,
171
+ data: { service: service, operation: operation, error: error.class.name }
172
+ )
173
+
174
+ return unless BrainzLab.configuration.flux_effectively_enabled?
175
+
176
+ tags = { service: service, operation: operation, error_class: error.class.name }
177
+ BrainzLab::Flux.increment('aws.errors', tags: tags)
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module DalliInstrumentation
6
+ TRACKED_COMMANDS = %w[get set add replace delete incr decr cas get_multi set_multi].freeze
7
+
8
+ class << self
9
+ def install!
10
+ return unless defined?(::Dalli::Client)
11
+
12
+ install_client_instrumentation!
13
+
14
+ BrainzLab.debug_log('[Instrumentation] Dalli/Memcached instrumentation installed')
15
+ end
16
+
17
+ private
18
+
19
+ def install_client_instrumentation!
20
+ ::Dalli::Client.class_eval do
21
+ TRACKED_COMMANDS.each do |cmd|
22
+ original_method = "original_#{cmd}"
23
+ next if method_defined?(original_method)
24
+
25
+ alias_method original_method, cmd
26
+
27
+ define_method(cmd) do |*args, &block|
28
+ BrainzLab::Instrumentation::DalliInstrumentation.track_command(cmd, args) do
29
+ send(original_method, *args, &block)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.track_command(command, args)
38
+ started_at = Time.now
39
+
40
+ begin
41
+ result = yield
42
+ track_success(command, args, started_at, result)
43
+ result
44
+ rescue StandardError => e
45
+ track_error(command, args, started_at, e)
46
+ raise
47
+ end
48
+ end
49
+
50
+ def self.track_success(command, args, started_at, result)
51
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
52
+ key = extract_key(args)
53
+
54
+ # Add breadcrumb
55
+ BrainzLab::Reflex.add_breadcrumb(
56
+ "Memcached #{command.upcase}",
57
+ category: 'cache',
58
+ level: :info,
59
+ data: { command: command, key: key, duration_ms: duration_ms }
60
+ )
61
+
62
+ # Track with Flux
63
+ return unless BrainzLab.configuration.flux_effectively_enabled?
64
+
65
+ tags = { command: command }
66
+ BrainzLab::Flux.distribution('memcached.duration_ms', duration_ms, tags: tags)
67
+ BrainzLab::Flux.increment('memcached.commands', tags: tags)
68
+
69
+ # Track cache hits/misses for get commands
70
+ return unless command == 'get'
71
+
72
+ if result.nil?
73
+ BrainzLab::Flux.increment('memcached.miss', tags: tags)
74
+ else
75
+ BrainzLab::Flux.increment('memcached.hit', tags: tags)
76
+ end
77
+ end
78
+
79
+ def self.track_error(command, args, started_at, error)
80
+ ((Time.now - started_at) * 1000).round(2)
81
+ key = extract_key(args)
82
+
83
+ BrainzLab::Reflex.add_breadcrumb(
84
+ "Memcached #{command.upcase} failed: #{error.message}",
85
+ category: 'cache',
86
+ level: :error,
87
+ data: { command: command, key: key, error: error.class.name }
88
+ )
89
+
90
+ return unless BrainzLab.configuration.flux_effectively_enabled?
91
+
92
+ BrainzLab::Flux.increment('memcached.errors', tags: { command: command })
93
+ end
94
+
95
+ def self.extract_key(args)
96
+ key = args.first
97
+ case key
98
+ when String
99
+ key.length > 50 ? "#{key[0..47]}..." : key
100
+ when Array
101
+ "[#{key.size} keys]"
102
+ else
103
+ key.to_s[0..50]
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -11,17 +11,13 @@ module BrainzLab
11
11
  return if @installed
12
12
 
13
13
  # Install lifecycle hooks
14
- if defined?(::Delayed::Worker)
15
- install_lifecycle_hooks!
16
- end
14
+ install_lifecycle_hooks! if defined?(::Delayed::Worker)
17
15
 
18
16
  # Install plugin if Delayed::Plugin is available
19
- if defined?(::Delayed::Plugin)
20
- ::Delayed::Worker.plugins << Plugin
21
- end
17
+ ::Delayed::Worker.plugins << Plugin if defined?(::Delayed::Plugin)
22
18
 
23
19
  @installed = true
24
- BrainzLab.debug_log("Delayed::Job instrumentation installed")
20
+ BrainzLab.debug_log('Delayed::Job instrumentation installed')
25
21
  end
26
22
 
27
23
  def installed?
@@ -35,15 +31,15 @@ module BrainzLab
35
31
  private
36
32
 
37
33
  def install_lifecycle_hooks!
38
- ::Delayed::Worker.lifecycle.around(:invoke_job) do |job, *args, &block|
34
+ ::Delayed::Worker.lifecycle.around(:invoke_job) do |job, *_args, &block|
39
35
  around_invoke(job, &block)
40
36
  end
41
37
 
42
- ::Delayed::Worker.lifecycle.after(:error) do |worker, job|
38
+ ::Delayed::Worker.lifecycle.after(:error) do |_worker, job|
43
39
  record_error(job)
44
40
  end
45
41
 
46
- ::Delayed::Worker.lifecycle.after(:failure) do |worker, job|
42
+ ::Delayed::Worker.lifecycle.after(:failure) do |_worker, job|
47
43
  record_failure(job)
48
44
  end
49
45
  rescue StandardError => e
@@ -53,7 +49,7 @@ module BrainzLab
53
49
  def around_invoke(job, &block)
54
50
  started_at = Time.now.utc
55
51
  job_name = extract_job_name(job)
56
- queue = job.queue || "default"
52
+ queue = job.queue || 'default'
57
53
 
58
54
  # Calculate queue wait time
59
55
  queue_wait_ms = job.created_at ? ((started_at - job.created_at) * 1000).round(2) : nil
@@ -64,7 +60,7 @@ module BrainzLab
64
60
  # Add breadcrumb
65
61
  BrainzLab::Reflex.add_breadcrumb(
66
62
  "DelayedJob #{job_name}",
67
- category: "job.delayed_job",
63
+ category: 'job.delayed_job',
68
64
  level: :info,
69
65
  data: { job_id: job.id, queue: queue, attempts: job.attempts }
70
66
  )
@@ -133,7 +129,7 @@ module BrainzLab
133
129
  payload = {
134
130
  trace_id: SecureRandom.uuid,
135
131
  name: job_name,
136
- kind: "job",
132
+ kind: 'job',
137
133
  started_at: started_at.utc.iso8601(3),
138
134
  ended_at: ended_at.utc.iso8601(3),
139
135
  duration_ms: duration_ms,
@@ -162,7 +158,7 @@ module BrainzLab
162
158
 
163
159
  BrainzLab::Reflex.add_breadcrumb(
164
160
  "DelayedJob error: #{extract_job_name(job)}",
165
- category: "job.delayed_job.error",
161
+ category: 'job.delayed_job.error',
166
162
  level: :error,
167
163
  data: {
168
164
  job_id: job.id,
@@ -177,7 +173,7 @@ module BrainzLab
177
173
  def record_failure(job)
178
174
  BrainzLab::Reflex.add_breadcrumb(
179
175
  "DelayedJob failed permanently: #{extract_job_name(job)}",
180
- category: "job.delayed_job.failure",
176
+ category: 'job.delayed_job.failure',
181
177
  level: :error,
182
178
  data: {
183
179
  job_id: job.id,
@@ -200,7 +196,7 @@ module BrainzLab
200
196
  payload.class.name
201
197
  end
202
198
  rescue StandardError
203
- job.name || "Unknown"
199
+ job.name || 'Unknown'
204
200
  end
205
201
 
206
202
  def format_timestamp(ts)
@@ -216,21 +212,23 @@ module BrainzLab
216
212
  end
217
213
 
218
214
  # Delayed::Job Plugin (alternative installation method)
219
- class Plugin < ::Delayed::Plugin
220
- callbacks do |lifecycle|
221
- lifecycle.around(:invoke_job) do |job, *args, &block|
222
- DelayedJobInstrumentation.send(:around_invoke, job, &block)
223
- end
224
-
225
- lifecycle.after(:error) do |worker, job|
226
- DelayedJobInstrumentation.send(:record_error, job)
227
- end
228
-
229
- lifecycle.after(:failure) do |worker, job|
230
- DelayedJobInstrumentation.send(:record_failure, job)
215
+ if defined?(::Delayed::Plugin)
216
+ class Plugin < ::Delayed::Plugin
217
+ callbacks do |lifecycle|
218
+ lifecycle.around(:invoke_job) do |job, *_args, &block|
219
+ DelayedJobInstrumentation.send(:around_invoke, job, &block)
220
+ end
221
+
222
+ lifecycle.after(:error) do |_worker, job|
223
+ DelayedJobInstrumentation.send(:record_error, job)
224
+ end
225
+
226
+ lifecycle.after(:failure) do |_worker, job|
227
+ DelayedJobInstrumentation.send(:record_failure, job)
228
+ end
231
229
  end
232
230
  end
233
- end if defined?(::Delayed::Plugin)
231
+ end
234
232
  end
235
233
  end
236
234
  end
@@ -32,7 +32,7 @@ module BrainzLab
32
32
  return unless installed_any
33
33
 
34
34
  @installed = true
35
- BrainzLab.debug_log("Elasticsearch instrumentation installed")
35
+ BrainzLab.debug_log('Elasticsearch instrumentation installed')
36
36
  end
37
37
 
38
38
  def installed?
@@ -64,14 +64,12 @@ module BrainzLab
64
64
  return super unless should_track?
65
65
 
66
66
  started_at = Time.now.utc
67
- error_info = nil
68
67
 
69
68
  begin
70
69
  response = super
71
70
  record_request(method, path, params, started_at, response.status)
72
71
  response
73
72
  rescue StandardError => e
74
- error_info = e
75
73
  record_request(method, path, params, started_at, nil, e)
76
74
  raise
77
75
  end
@@ -83,7 +81,7 @@ module BrainzLab
83
81
  BrainzLab.configuration.instrument_elasticsearch
84
82
  end
85
83
 
86
- def record_request(method, path, params, started_at, status, error = nil)
84
+ def record_request(method, path, _params, started_at, status, error = nil)
87
85
  duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
88
86
  operation = extract_operation(method, path)
89
87
  index = extract_index(path)
@@ -93,7 +91,7 @@ module BrainzLab
93
91
  if BrainzLab.configuration.reflex_enabled
94
92
  BrainzLab::Reflex.add_breadcrumb(
95
93
  "ES #{operation}",
96
- category: "elasticsearch",
94
+ category: 'elasticsearch',
97
95
  level: level,
98
96
  data: {
99
97
  method: method.to_s.upcase,
@@ -143,7 +141,7 @@ module BrainzLab
143
141
  span = {
144
142
  span_id: SecureRandom.uuid,
145
143
  name: "ES #{operation}",
146
- kind: "elasticsearch",
144
+ kind: 'elasticsearch',
147
145
  started_at: started_at,
148
146
  ended_at: Time.now.utc,
149
147
  duration_ms: duration_ms,
@@ -168,25 +166,25 @@ module BrainzLab
168
166
  method_str = method.to_s.upcase
169
167
 
170
168
  case path
171
- when %r{/_search} then "search"
172
- when %r{/_bulk} then "bulk"
173
- when %r{/_count} then "count"
174
- when %r{/_mget} then "mget"
175
- when %r{/_msearch} then "msearch"
176
- when %r{/_update_by_query} then "update_by_query"
177
- when %r{/_delete_by_query} then "delete_by_query"
178
- when %r{/_refresh} then "refresh"
179
- when %r{/_mapping} then "mapping"
180
- when %r{/_settings} then "settings"
181
- when %r{/_alias} then "alias"
182
- when %r{/_analyze} then "analyze"
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'
183
181
  else
184
182
  case method_str
185
- when "GET" then "get"
186
- when "POST" then "index"
187
- when "PUT" then "update"
188
- when "DELETE" then "delete"
189
- when "HEAD" then "exists"
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'
190
188
  else method_str.downcase
191
189
  end
192
190
  end
@@ -195,13 +193,14 @@ module BrainzLab
195
193
  def extract_index(path)
196
194
  # Extract index name from path like /my-index/_search
197
195
  match = path.match(%r{^/([^/_]+)})
198
- match[1] if match && !match[1].start_with?("_")
196
+ match[1] if match && !match[1].start_with?('_')
199
197
  rescue StandardError
200
198
  nil
201
199
  end
202
200
 
203
201
  def truncate_path(path)
204
202
  return nil unless path
203
+
205
204
  path.to_s[0, 200]
206
205
  end
207
206
  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