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,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module GraphQLInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::GraphQL::Schema)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
# For GraphQL Ruby 2.0+
|
|
14
|
+
if ::GraphQL::Schema.respond_to?(:trace_with)
|
|
15
|
+
# Will be installed per-schema via BrainzLab::GraphQL::Tracer
|
|
16
|
+
BrainzLab.debug_log("GraphQL tracer available - add `trace_with BrainzLab::Instrumentation::GraphQLInstrumentation::Tracer` to your schema")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Subscribe to ActiveSupport notifications if available
|
|
20
|
+
install_notifications!
|
|
21
|
+
|
|
22
|
+
@installed = true
|
|
23
|
+
BrainzLab.debug_log("GraphQL instrumentation installed")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def installed?
|
|
27
|
+
@installed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset!
|
|
31
|
+
@installed = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def install_notifications!
|
|
37
|
+
# GraphQL-ruby emits ActiveSupport notifications
|
|
38
|
+
ActiveSupport::Notifications.subscribe("execute.graphql") do |*args|
|
|
39
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
40
|
+
record_execution(event)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ActiveSupport::Notifications.subscribe("analyze.graphql") do |*args|
|
|
44
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
45
|
+
record_analyze(event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ActiveSupport::Notifications.subscribe("validate.graphql") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
record_validate(event)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
BrainzLab.debug_log("GraphQL notifications setup failed: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_execution(event)
|
|
57
|
+
payload = event.payload
|
|
58
|
+
query = payload[:query]
|
|
59
|
+
operation_name = query&.operation_name || "anonymous"
|
|
60
|
+
operation_type = query&.selected_operation&.operation_type || "query"
|
|
61
|
+
duration_ms = event.duration.round(2)
|
|
62
|
+
|
|
63
|
+
# Add breadcrumb
|
|
64
|
+
if BrainzLab.configuration.reflex_enabled
|
|
65
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
66
|
+
"GraphQL #{operation_type} #{operation_name}",
|
|
67
|
+
category: "graphql.execute",
|
|
68
|
+
level: payload[:errors]&.any? ? :error : :info,
|
|
69
|
+
data: {
|
|
70
|
+
operation_name: operation_name,
|
|
71
|
+
operation_type: operation_type,
|
|
72
|
+
duration_ms: duration_ms,
|
|
73
|
+
error_count: payload[:errors]&.size || 0
|
|
74
|
+
}.compact
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Record span
|
|
79
|
+
record_span(
|
|
80
|
+
name: "GraphQL #{operation_type} #{operation_name}",
|
|
81
|
+
kind: "graphql",
|
|
82
|
+
duration_ms: duration_ms,
|
|
83
|
+
started_at: event.time,
|
|
84
|
+
ended_at: event.end,
|
|
85
|
+
data: {
|
|
86
|
+
operation_name: operation_name,
|
|
87
|
+
operation_type: operation_type,
|
|
88
|
+
query: truncate_query(query&.query_string),
|
|
89
|
+
variables: sanitize_variables(query&.variables&.to_h),
|
|
90
|
+
error_count: payload[:errors]&.size || 0
|
|
91
|
+
}.compact,
|
|
92
|
+
error: payload[:errors]&.any?
|
|
93
|
+
)
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
BrainzLab.debug_log("GraphQL execution recording failed: #{e.message}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def record_analyze(event)
|
|
99
|
+
record_span(
|
|
100
|
+
name: "GraphQL analyze",
|
|
101
|
+
kind: "graphql",
|
|
102
|
+
duration_ms: event.duration.round(2),
|
|
103
|
+
started_at: event.time,
|
|
104
|
+
ended_at: event.end,
|
|
105
|
+
data: { phase: "analyze" }
|
|
106
|
+
)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
BrainzLab.debug_log("GraphQL analyze recording failed: #{e.message}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def record_validate(event)
|
|
112
|
+
record_span(
|
|
113
|
+
name: "GraphQL validate",
|
|
114
|
+
kind: "graphql",
|
|
115
|
+
duration_ms: event.duration.round(2),
|
|
116
|
+
started_at: event.time,
|
|
117
|
+
ended_at: event.end,
|
|
118
|
+
data: { phase: "validate" }
|
|
119
|
+
)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
BrainzLab.debug_log("GraphQL validate recording failed: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def record_span(name:, kind:, duration_ms:, started_at:, ended_at:, data:, error: false)
|
|
125
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
126
|
+
return unless spans
|
|
127
|
+
|
|
128
|
+
spans << {
|
|
129
|
+
span_id: SecureRandom.uuid,
|
|
130
|
+
name: name,
|
|
131
|
+
kind: kind,
|
|
132
|
+
started_at: started_at,
|
|
133
|
+
ended_at: ended_at,
|
|
134
|
+
duration_ms: duration_ms,
|
|
135
|
+
data: data,
|
|
136
|
+
error: error
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def truncate_query(query)
|
|
141
|
+
return nil unless query
|
|
142
|
+
query.to_s[0, 2000]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def sanitize_variables(variables)
|
|
146
|
+
return nil unless variables
|
|
147
|
+
|
|
148
|
+
scrub_fields = BrainzLab.configuration.scrub_fields
|
|
149
|
+
variables.transform_values do |value|
|
|
150
|
+
if scrub_fields.any? { |f| value.to_s.downcase.include?(f.to_s) }
|
|
151
|
+
"[FILTERED]"
|
|
152
|
+
else
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
rescue StandardError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# GraphQL Ruby 2.0+ Tracer module
|
|
162
|
+
# Add to your schema: trace_with BrainzLab::Instrumentation::GraphQLInstrumentation::Tracer
|
|
163
|
+
module Tracer
|
|
164
|
+
def execute_query(query:)
|
|
165
|
+
started_at = Time.now.utc
|
|
166
|
+
operation_name = query.operation_name || "anonymous"
|
|
167
|
+
operation_type = query.selected_operation&.operation_type || "query"
|
|
168
|
+
|
|
169
|
+
result = super
|
|
170
|
+
|
|
171
|
+
duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
|
|
172
|
+
has_errors = result.to_h["errors"]&.any?
|
|
173
|
+
|
|
174
|
+
# Add breadcrumb
|
|
175
|
+
if BrainzLab.configuration.reflex_enabled
|
|
176
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
177
|
+
"GraphQL #{operation_type} #{operation_name}",
|
|
178
|
+
category: "graphql.execute",
|
|
179
|
+
level: has_errors ? :error : :info,
|
|
180
|
+
data: {
|
|
181
|
+
operation_name: operation_name,
|
|
182
|
+
operation_type: operation_type,
|
|
183
|
+
duration_ms: duration_ms
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Record span
|
|
189
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
190
|
+
if spans
|
|
191
|
+
spans << {
|
|
192
|
+
span_id: SecureRandom.uuid,
|
|
193
|
+
name: "GraphQL #{operation_type} #{operation_name}",
|
|
194
|
+
kind: "graphql",
|
|
195
|
+
started_at: started_at,
|
|
196
|
+
ended_at: Time.now.utc,
|
|
197
|
+
duration_ms: duration_ms,
|
|
198
|
+
data: {
|
|
199
|
+
operation_name: operation_name,
|
|
200
|
+
operation_type: operation_type
|
|
201
|
+
},
|
|
202
|
+
error: has_errors
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
result
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
# Record error
|
|
209
|
+
if BrainzLab.configuration.reflex_enabled
|
|
210
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
211
|
+
"GraphQL #{operation_type} #{operation_name} failed",
|
|
212
|
+
category: "graphql.error",
|
|
213
|
+
level: :error,
|
|
214
|
+
data: { error: e.class.name }
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
raise
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def execute_field(field:, query:, ast_node:, arguments:, object:)
|
|
221
|
+
started_at = Time.now.utc
|
|
222
|
+
|
|
223
|
+
result = super
|
|
224
|
+
|
|
225
|
+
duration_ms = ((Time.now.utc - started_at) * 1000).round(2)
|
|
226
|
+
|
|
227
|
+
# Only track slow field resolutions (> 10ms) to avoid noise
|
|
228
|
+
if duration_ms > 10
|
|
229
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
230
|
+
if spans
|
|
231
|
+
spans << {
|
|
232
|
+
span_id: SecureRandom.uuid,
|
|
233
|
+
name: "GraphQL field #{field.owner.graphql_name}.#{field.graphql_name}",
|
|
234
|
+
kind: "graphql.field",
|
|
235
|
+
started_at: started_at,
|
|
236
|
+
ended_at: Time.now.utc,
|
|
237
|
+
duration_ms: duration_ms,
|
|
238
|
+
data: {
|
|
239
|
+
field: field.graphql_name,
|
|
240
|
+
parent_type: field.owner.graphql_name
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
result
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
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 = {}, &block)
|
|
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
|
+
error_info = nil
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
response = super
|
|
40
|
+
track_request(http_method, path, options, response.code, started_at)
|
|
41
|
+
response
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
error_info = e.class.name
|
|
44
|
+
track_request(http_method, path, options, nil, started_at, error_info)
|
|
45
|
+
raise
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def should_track?(path, options)
|
|
52
|
+
return false unless BrainzLab.configuration.instrument_http
|
|
53
|
+
|
|
54
|
+
uri = parse_uri(path, options)
|
|
55
|
+
return true unless uri
|
|
56
|
+
|
|
57
|
+
ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
|
|
58
|
+
!ignore_hosts.include?(uri.host)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def inject_trace_context(options)
|
|
62
|
+
return options unless BrainzLab.configuration.pulse_enabled
|
|
63
|
+
|
|
64
|
+
options = options.dup
|
|
65
|
+
options[:headers] ||= {}
|
|
66
|
+
|
|
67
|
+
trace_headers = {}
|
|
68
|
+
BrainzLab::Pulse.inject(trace_headers, format: :all)
|
|
69
|
+
|
|
70
|
+
options[:headers] = options[:headers].merge(trace_headers)
|
|
71
|
+
options
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
|
|
74
|
+
options
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def track_request(http_method, path, options, status, started_at, error = nil)
|
|
78
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
79
|
+
method = extract_method_name(http_method)
|
|
80
|
+
uri = parse_uri(path, options)
|
|
81
|
+
url = uri ? sanitize_url(uri) : path.to_s
|
|
82
|
+
host = uri&.host || "unknown"
|
|
83
|
+
request_path = uri&.path || path.to_s
|
|
84
|
+
level = error || (status && status >= 400) ? :error : :info
|
|
85
|
+
|
|
86
|
+
# Add breadcrumb for Reflex
|
|
87
|
+
if BrainzLab.configuration.reflex_enabled
|
|
88
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
89
|
+
"#{method} #{url}",
|
|
90
|
+
category: "http.httparty",
|
|
91
|
+
level: level,
|
|
92
|
+
data: {
|
|
93
|
+
method: method,
|
|
94
|
+
url: url,
|
|
95
|
+
host: host,
|
|
96
|
+
path: request_path,
|
|
97
|
+
status_code: status,
|
|
98
|
+
duration_ms: duration_ms,
|
|
99
|
+
error: error
|
|
100
|
+
}.compact
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Record span for Pulse APM
|
|
105
|
+
record_pulse_span(method, host, request_path, status, duration_ms, error)
|
|
106
|
+
|
|
107
|
+
# Log to Recall at debug level
|
|
108
|
+
if BrainzLab.configuration.recall_enabled
|
|
109
|
+
BrainzLab::Recall.debug(
|
|
110
|
+
"HTTP #{method} #{url} -> #{status || 'ERROR'}",
|
|
111
|
+
method: method,
|
|
112
|
+
url: url,
|
|
113
|
+
host: host,
|
|
114
|
+
status_code: status,
|
|
115
|
+
duration_ms: duration_ms,
|
|
116
|
+
error: error
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
BrainzLab.debug_log("HTTParty instrumentation error: #{e.message}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def record_pulse_span(method, host, path, status, duration_ms, error)
|
|
124
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
125
|
+
return unless spans
|
|
126
|
+
|
|
127
|
+
span = {
|
|
128
|
+
span_id: SecureRandom.uuid,
|
|
129
|
+
name: "HTTP #{method} #{host}",
|
|
130
|
+
kind: "http",
|
|
131
|
+
started_at: Time.now.utc - (duration_ms / 1000.0),
|
|
132
|
+
ended_at: Time.now.utc,
|
|
133
|
+
duration_ms: duration_ms,
|
|
134
|
+
data: {
|
|
135
|
+
method: method,
|
|
136
|
+
host: host,
|
|
137
|
+
path: path,
|
|
138
|
+
status: status
|
|
139
|
+
}.compact
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if error
|
|
143
|
+
span[:error] = true
|
|
144
|
+
span[:error_class] = error
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
spans << span
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def extract_method_name(http_method)
|
|
151
|
+
case http_method.name
|
|
152
|
+
when /Get$/ then "GET"
|
|
153
|
+
when /Post$/ then "POST"
|
|
154
|
+
when /Put$/ then "PUT"
|
|
155
|
+
when /Patch$/ then "PATCH"
|
|
156
|
+
when /Delete$/ then "DELETE"
|
|
157
|
+
when /Head$/ then "HEAD"
|
|
158
|
+
when /Options$/ then "OPTIONS"
|
|
159
|
+
else http_method.name.split("::").last.upcase
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def parse_uri(path, options)
|
|
164
|
+
base_uri = options[:base_uri]
|
|
165
|
+
if base_uri
|
|
166
|
+
URI.join(base_uri.to_s, path.to_s)
|
|
167
|
+
else
|
|
168
|
+
URI.parse(path.to_s)
|
|
169
|
+
end
|
|
170
|
+
rescue URI::InvalidURIError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def sanitize_url(uri)
|
|
175
|
+
result = uri.dup
|
|
176
|
+
if result.query
|
|
177
|
+
params = URI.decode_www_form(result.query).reject do |key, _|
|
|
178
|
+
sensitive_param?(key)
|
|
179
|
+
end
|
|
180
|
+
result.query = params.empty? ? nil : URI.encode_www_form(params)
|
|
181
|
+
end
|
|
182
|
+
result.to_s
|
|
183
|
+
rescue StandardError
|
|
184
|
+
uri.to_s
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def sensitive_param?(key)
|
|
188
|
+
key = key.to_s.downcase
|
|
189
|
+
%w[token api_key apikey secret password auth key].any? { |s| key.include?(s) }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
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
|
+
if ::Mongoid.respond_to?(:subscribe)
|
|
55
|
+
::Mongoid.subscribe(CommandSubscriber.new)
|
|
56
|
+
end
|
|
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
|