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,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
|