brainzlab 0.1.0
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 +52 -0
- data/LICENSE +26 -0
- data/README.md +311 -0
- data/lib/brainzlab/configuration.rb +215 -0
- data/lib/brainzlab/context.rb +91 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
- data/lib/brainzlab/instrumentation/active_record.rb +111 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
- data/lib/brainzlab/instrumentation/faraday.rb +182 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +251 -0
- data/lib/brainzlab/instrumentation/httparty.rb +194 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +109 -0
- data/lib/brainzlab/instrumentation/redis.rb +331 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
- data/lib/brainzlab/instrumentation.rb +132 -0
- data/lib/brainzlab/pulse/client.rb +132 -0
- data/lib/brainzlab/pulse/instrumentation.rb +364 -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 +224 -0
- data/lib/brainzlab/rails/log_formatter.rb +801 -0
- data/lib/brainzlab/rails/log_subscriber.rb +341 -0
- data/lib/brainzlab/rails/railtie.rb +590 -0
- data/lib/brainzlab/recall/buffer.rb +64 -0
- data/lib/brainzlab/recall/client.rb +86 -0
- data/lib/brainzlab/recall/logger.rb +118 -0
- data/lib/brainzlab/recall/provisioner.rb +113 -0
- data/lib/brainzlab/recall.rb +155 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +85 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +374 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +140 -0
- data/lib/generators/brainzlab/install/install_generator.rb +61 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +159 -0
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
error_info = nil
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
response = super
|
|
71
|
+
record_request(method, path, params, started_at, response.status)
|
|
72
|
+
response
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
error_info = e
|
|
75
|
+
record_request(method, path, params, started_at, nil, e)
|
|
76
|
+
raise
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def should_track?
|
|
83
|
+
BrainzLab.configuration.instrument_elasticsearch
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def record_request(method, path, params, started_at, status, error = nil)
|
|
87
|
+
duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
|
|
88
|
+
operation = extract_operation(method, path)
|
|
89
|
+
index = extract_index(path)
|
|
90
|
+
level = error || (status && status >= 400) ? :error : :info
|
|
91
|
+
|
|
92
|
+
# Add breadcrumb for Reflex
|
|
93
|
+
if BrainzLab.configuration.reflex_enabled
|
|
94
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
95
|
+
"ES #{operation}",
|
|
96
|
+
category: "elasticsearch",
|
|
97
|
+
level: level,
|
|
98
|
+
data: {
|
|
99
|
+
method: method.to_s.upcase,
|
|
100
|
+
path: truncate_path(path),
|
|
101
|
+
index: index,
|
|
102
|
+
status: status,
|
|
103
|
+
duration_ms: duration_ms,
|
|
104
|
+
error: error&.class&.name
|
|
105
|
+
}.compact
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Record span for Pulse
|
|
110
|
+
record_span(
|
|
111
|
+
operation: operation,
|
|
112
|
+
method: method,
|
|
113
|
+
path: path,
|
|
114
|
+
index: index,
|
|
115
|
+
started_at: started_at,
|
|
116
|
+
duration_ms: duration_ms,
|
|
117
|
+
status: status,
|
|
118
|
+
error: error
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Log to Recall
|
|
122
|
+
if BrainzLab.configuration.recall_enabled
|
|
123
|
+
log_method = error ? :warn : :debug
|
|
124
|
+
BrainzLab::Recall.send(
|
|
125
|
+
log_method,
|
|
126
|
+
"ES #{method.to_s.upcase} #{path} -> #{status || 'ERROR'} (#{duration_ms}ms)",
|
|
127
|
+
method: method.to_s.upcase,
|
|
128
|
+
path: path,
|
|
129
|
+
index: index,
|
|
130
|
+
status: status,
|
|
131
|
+
duration_ms: duration_ms,
|
|
132
|
+
error: error&.message
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
BrainzLab.debug_log("Elasticsearch recording failed: #{e.message}")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def record_span(operation:, method:, path:, index:, started_at:, duration_ms:, status:, error:)
|
|
140
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
141
|
+
return unless spans
|
|
142
|
+
|
|
143
|
+
span = {
|
|
144
|
+
span_id: SecureRandom.uuid,
|
|
145
|
+
name: "ES #{operation}",
|
|
146
|
+
kind: "elasticsearch",
|
|
147
|
+
started_at: started_at,
|
|
148
|
+
ended_at: Time.now.utc,
|
|
149
|
+
duration_ms: duration_ms,
|
|
150
|
+
data: {
|
|
151
|
+
method: method.to_s.upcase,
|
|
152
|
+
path: truncate_path(path),
|
|
153
|
+
index: index,
|
|
154
|
+
status: status
|
|
155
|
+
}.compact
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if error
|
|
159
|
+
span[:error] = true
|
|
160
|
+
span[:error_class] = error.class.name
|
|
161
|
+
span[:error_message] = error.message&.slice(0, 500)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
spans << span
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_operation(method, path)
|
|
168
|
+
method_str = method.to_s.upcase
|
|
169
|
+
|
|
170
|
+
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"
|
|
183
|
+
else
|
|
184
|
+
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"
|
|
190
|
+
else method_str.downcase
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def extract_index(path)
|
|
196
|
+
# Extract index name from path like /my-index/_search
|
|
197
|
+
match = path.match(%r{^/([^/_]+)})
|
|
198
|
+
match[1] if match && !match[1].start_with?("_")
|
|
199
|
+
rescue StandardError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def truncate_path(path)
|
|
204
|
+
return nil unless path
|
|
205
|
+
path.to_s[0, 200]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
error_info = nil
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
response = @app.call(env)
|
|
53
|
+
track_request(env, response.status, started_at)
|
|
54
|
+
response
|
|
55
|
+
rescue ::Faraday::Error => e
|
|
56
|
+
error_info = e.class.name
|
|
57
|
+
track_request(env, e.response&.dig(:status), started_at, error_info)
|
|
58
|
+
raise
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
error_info = e.class.name
|
|
61
|
+
track_request(env, nil, started_at, error_info)
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def should_track?(env)
|
|
69
|
+
return false unless BrainzLab.configuration.instrument_http
|
|
70
|
+
|
|
71
|
+
ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
|
|
72
|
+
host = env.url.host
|
|
73
|
+
!ignore_hosts.include?(host)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inject_trace_context(env)
|
|
77
|
+
return unless BrainzLab.configuration.pulse_enabled
|
|
78
|
+
|
|
79
|
+
headers = {}
|
|
80
|
+
BrainzLab::Pulse.inject(headers, format: :all)
|
|
81
|
+
|
|
82
|
+
headers.each do |key, value|
|
|
83
|
+
env.request_headers[key] = value
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def track_request(env, status, started_at, error = nil)
|
|
90
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
91
|
+
method = env.method.to_s.upcase
|
|
92
|
+
url = sanitize_url(env.url)
|
|
93
|
+
host = env.url.host
|
|
94
|
+
path = env.url.path
|
|
95
|
+
level = error || (status && status >= 400) ? :error : :info
|
|
96
|
+
|
|
97
|
+
# Add breadcrumb for Reflex
|
|
98
|
+
if BrainzLab.configuration.reflex_enabled
|
|
99
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
100
|
+
"#{method} #{url}",
|
|
101
|
+
category: "http.faraday",
|
|
102
|
+
level: level,
|
|
103
|
+
data: {
|
|
104
|
+
method: method,
|
|
105
|
+
url: url,
|
|
106
|
+
host: host,
|
|
107
|
+
path: path,
|
|
108
|
+
status_code: status,
|
|
109
|
+
duration_ms: duration_ms,
|
|
110
|
+
error: error
|
|
111
|
+
}.compact
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Record span for Pulse APM
|
|
116
|
+
record_pulse_span(method, host, path, status, duration_ms, error)
|
|
117
|
+
|
|
118
|
+
# Log to Recall at debug level
|
|
119
|
+
if BrainzLab.configuration.recall_enabled
|
|
120
|
+
BrainzLab::Recall.debug(
|
|
121
|
+
"HTTP #{method} #{url} -> #{status || 'ERROR'}",
|
|
122
|
+
method: method,
|
|
123
|
+
url: url,
|
|
124
|
+
host: host,
|
|
125
|
+
status_code: status,
|
|
126
|
+
duration_ms: duration_ms,
|
|
127
|
+
error: error
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
BrainzLab.debug_log("Faraday instrumentation error: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def record_pulse_span(method, host, path, status, duration_ms, error)
|
|
135
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
136
|
+
return unless spans
|
|
137
|
+
|
|
138
|
+
span = {
|
|
139
|
+
span_id: SecureRandom.uuid,
|
|
140
|
+
name: "HTTP #{method} #{host}",
|
|
141
|
+
kind: "http",
|
|
142
|
+
started_at: Time.now.utc - (duration_ms / 1000.0),
|
|
143
|
+
ended_at: Time.now.utc,
|
|
144
|
+
duration_ms: duration_ms,
|
|
145
|
+
data: {
|
|
146
|
+
method: method,
|
|
147
|
+
host: host,
|
|
148
|
+
path: path,
|
|
149
|
+
status: status
|
|
150
|
+
}.compact
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if error
|
|
154
|
+
span[:error] = true
|
|
155
|
+
span[:error_class] = error
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
spans << span
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def sanitize_url(url)
|
|
162
|
+
# Remove sensitive query parameters
|
|
163
|
+
uri = url.dup
|
|
164
|
+
if uri.query
|
|
165
|
+
params = URI.decode_www_form(uri.query).reject do |key, _|
|
|
166
|
+
sensitive_param?(key)
|
|
167
|
+
end
|
|
168
|
+
uri.query = params.empty? ? nil : URI.encode_www_form(params)
|
|
169
|
+
end
|
|
170
|
+
uri.to_s
|
|
171
|
+
rescue StandardError
|
|
172
|
+
url.to_s
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def sensitive_param?(key)
|
|
176
|
+
key = key.to_s.downcase
|
|
177
|
+
%w[token api_key apikey secret password auth key].any? { |s| key.include?(s) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module GrapeInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::Grape::API)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
# Subscribe to Grape's ActiveSupport notifications
|
|
14
|
+
install_notifications!
|
|
15
|
+
|
|
16
|
+
@installed = true
|
|
17
|
+
BrainzLab.debug_log("Grape instrumentation installed")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def installed?
|
|
21
|
+
@installed
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset!
|
|
25
|
+
@installed = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def install_notifications!
|
|
31
|
+
# Grape emits these notifications
|
|
32
|
+
ActiveSupport::Notifications.subscribe("endpoint_run.grape") do |*args|
|
|
33
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
34
|
+
record_endpoint(event)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ActiveSupport::Notifications.subscribe("endpoint_render.grape") do |*args|
|
|
38
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
39
|
+
record_render(event)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ActiveSupport::Notifications.subscribe("endpoint_run_filters.grape") do |*args|
|
|
43
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
44
|
+
record_filters(event)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Format validation
|
|
48
|
+
ActiveSupport::Notifications.subscribe("format_response.grape") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
record_format(event)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
BrainzLab.debug_log("Grape notifications setup failed: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_endpoint(event)
|
|
57
|
+
payload = event.payload
|
|
58
|
+
endpoint = payload[:endpoint]
|
|
59
|
+
env = payload[:env] || {}
|
|
60
|
+
|
|
61
|
+
method = env["REQUEST_METHOD"] || "GET"
|
|
62
|
+
path = endpoint&.options&.dig(:path)&.first || env["PATH_INFO"] || "/"
|
|
63
|
+
route_pattern = extract_route_pattern(endpoint)
|
|
64
|
+
duration_ms = event.duration.round(2)
|
|
65
|
+
|
|
66
|
+
status = env["api.endpoint"]&.status || 200
|
|
67
|
+
level = status >= 400 ? :error : :info
|
|
68
|
+
|
|
69
|
+
# Add breadcrumb for Reflex
|
|
70
|
+
if BrainzLab.configuration.reflex_enabled
|
|
71
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
72
|
+
"Grape #{method} #{route_pattern}",
|
|
73
|
+
category: "grape.endpoint",
|
|
74
|
+
level: level,
|
|
75
|
+
data: {
|
|
76
|
+
method: method,
|
|
77
|
+
path: path,
|
|
78
|
+
route: route_pattern,
|
|
79
|
+
status: status,
|
|
80
|
+
duration_ms: duration_ms
|
|
81
|
+
}.compact
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Record span for Pulse
|
|
86
|
+
record_span(
|
|
87
|
+
name: "Grape #{method} #{route_pattern}",
|
|
88
|
+
kind: "grape",
|
|
89
|
+
started_at: event.time,
|
|
90
|
+
ended_at: event.end,
|
|
91
|
+
duration_ms: duration_ms,
|
|
92
|
+
data: {
|
|
93
|
+
method: method,
|
|
94
|
+
path: path,
|
|
95
|
+
route: route_pattern,
|
|
96
|
+
status: status
|
|
97
|
+
}.compact,
|
|
98
|
+
error: status >= 500
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Log to Recall
|
|
102
|
+
if BrainzLab.configuration.recall_enabled
|
|
103
|
+
BrainzLab::Recall.info(
|
|
104
|
+
"Grape #{method} #{path} -> #{status} (#{duration_ms}ms)",
|
|
105
|
+
method: method,
|
|
106
|
+
path: path,
|
|
107
|
+
route: route_pattern,
|
|
108
|
+
status: status,
|
|
109
|
+
duration_ms: duration_ms
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
BrainzLab.debug_log("Grape endpoint recording failed: #{e.message}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def record_render(event)
|
|
117
|
+
duration_ms = event.duration.round(2)
|
|
118
|
+
|
|
119
|
+
record_span(
|
|
120
|
+
name: "Grape render",
|
|
121
|
+
kind: "grape.render",
|
|
122
|
+
started_at: event.time,
|
|
123
|
+
ended_at: event.end,
|
|
124
|
+
duration_ms: duration_ms,
|
|
125
|
+
data: { phase: "render" }
|
|
126
|
+
)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
BrainzLab.debug_log("Grape render recording failed: #{e.message}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def record_filters(event)
|
|
132
|
+
payload = event.payload
|
|
133
|
+
duration_ms = event.duration.round(2)
|
|
134
|
+
filter_type = payload[:type] || "filter"
|
|
135
|
+
|
|
136
|
+
record_span(
|
|
137
|
+
name: "Grape #{filter_type} filters",
|
|
138
|
+
kind: "grape.filter",
|
|
139
|
+
started_at: event.time,
|
|
140
|
+
ended_at: event.end,
|
|
141
|
+
duration_ms: duration_ms,
|
|
142
|
+
data: { type: filter_type }
|
|
143
|
+
)
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
BrainzLab.debug_log("Grape filters recording failed: #{e.message}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_format(event)
|
|
149
|
+
duration_ms = event.duration.round(2)
|
|
150
|
+
|
|
151
|
+
record_span(
|
|
152
|
+
name: "Grape format response",
|
|
153
|
+
kind: "grape.format",
|
|
154
|
+
started_at: event.time,
|
|
155
|
+
ended_at: event.end,
|
|
156
|
+
duration_ms: duration_ms,
|
|
157
|
+
data: { phase: "format" }
|
|
158
|
+
)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
BrainzLab.debug_log("Grape format recording failed: #{e.message}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def record_span(name:, kind:, started_at:, ended_at:, duration_ms:, data:, error: false)
|
|
164
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
165
|
+
return unless spans
|
|
166
|
+
|
|
167
|
+
spans << {
|
|
168
|
+
span_id: SecureRandom.uuid,
|
|
169
|
+
name: name,
|
|
170
|
+
kind: kind,
|
|
171
|
+
started_at: started_at,
|
|
172
|
+
ended_at: ended_at,
|
|
173
|
+
duration_ms: duration_ms,
|
|
174
|
+
data: data,
|
|
175
|
+
error: error
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def extract_route_pattern(endpoint)
|
|
180
|
+
return "/" unless endpoint
|
|
181
|
+
|
|
182
|
+
route = endpoint.route
|
|
183
|
+
return "/" unless route
|
|
184
|
+
|
|
185
|
+
route.pattern&.path || route.path || "/"
|
|
186
|
+
rescue StandardError
|
|
187
|
+
"/"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Middleware for Grape (alternative installation)
|
|
192
|
+
# Usage: use BrainzLab::Instrumentation::GrapeInstrumentation::Middleware
|
|
193
|
+
class Middleware
|
|
194
|
+
def initialize(app)
|
|
195
|
+
@app = app
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def call(env)
|
|
199
|
+
return @app.call(env) unless should_trace?
|
|
200
|
+
|
|
201
|
+
started_at = Time.now.utc
|
|
202
|
+
request = Rack::Request.new(env)
|
|
203
|
+
|
|
204
|
+
# Initialize Pulse tracing
|
|
205
|
+
Thread.current[:brainzlab_pulse_spans] = []
|
|
206
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
207
|
+
|
|
208
|
+
# Extract parent trace context
|
|
209
|
+
parent_context = BrainzLab::Pulse.extract!(env)
|
|
210
|
+
|
|
211
|
+
begin
|
|
212
|
+
status, headers, response = @app.call(env)
|
|
213
|
+
|
|
214
|
+
record_trace(request, env, started_at, status, parent_context)
|
|
215
|
+
|
|
216
|
+
[status, headers, response]
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
record_trace(request, env, started_at, 500, parent_context, e)
|
|
219
|
+
raise
|
|
220
|
+
ensure
|
|
221
|
+
cleanup_context
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def should_trace?
|
|
228
|
+
BrainzLab.configuration.pulse_enabled
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def cleanup_context
|
|
232
|
+
Thread.current[:brainzlab_pulse_spans] = nil
|
|
233
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
234
|
+
BrainzLab::Context.clear!
|
|
235
|
+
BrainzLab::Pulse::Propagation.clear!
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def record_trace(request, env, started_at, status, parent_context, error = nil)
|
|
239
|
+
ended_at = Time.now.utc
|
|
240
|
+
duration_ms = ((ended_at - started_at) * 1000).round(2)
|
|
241
|
+
|
|
242
|
+
method = request.request_method
|
|
243
|
+
path = request.path
|
|
244
|
+
|
|
245
|
+
# Get route pattern from Grape if available
|
|
246
|
+
route_pattern = env["grape.routing_args"]&.dig(:route_info)&.pattern&.path || path
|
|
247
|
+
|
|
248
|
+
spans = Thread.current[:brainzlab_pulse_spans] || []
|
|
249
|
+
|
|
250
|
+
payload = {
|
|
251
|
+
trace_id: SecureRandom.uuid,
|
|
252
|
+
name: "#{method} #{route_pattern}",
|
|
253
|
+
kind: "request",
|
|
254
|
+
started_at: started_at.utc.iso8601(3),
|
|
255
|
+
ended_at: ended_at.utc.iso8601(3),
|
|
256
|
+
duration_ms: duration_ms,
|
|
257
|
+
request_method: method,
|
|
258
|
+
request_path: path,
|
|
259
|
+
status: status,
|
|
260
|
+
error: error.present? || status >= 500,
|
|
261
|
+
error_class: error&.class&.name,
|
|
262
|
+
error_message: error&.message&.slice(0, 1000),
|
|
263
|
+
spans: spans.map { |s| format_span(s) },
|
|
264
|
+
environment: BrainzLab.configuration.environment,
|
|
265
|
+
commit: BrainzLab.configuration.commit,
|
|
266
|
+
host: BrainzLab.configuration.host
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if parent_context&.valid?
|
|
270
|
+
payload[:parent_trace_id] = parent_context.trace_id
|
|
271
|
+
payload[:parent_span_id] = parent_context.span_id
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
BrainzLab::Pulse.client.send_trace(payload.compact)
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
BrainzLab.debug_log("Grape trace recording failed: #{e.message}")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def format_span(span)
|
|
280
|
+
{
|
|
281
|
+
span_id: span[:span_id],
|
|
282
|
+
name: span[:name],
|
|
283
|
+
kind: span[:kind],
|
|
284
|
+
started_at: span[:started_at]&.utc&.iso8601(3),
|
|
285
|
+
ended_at: span[:ended_at]&.utc&.iso8601(3),
|
|
286
|
+
duration_ms: span[:duration_ms],
|
|
287
|
+
data: span[:data]
|
|
288
|
+
}.compact
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|