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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- 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
|