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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
class << self
|
|
6
|
+
def install!
|
|
7
|
+
config = BrainzLab.configuration
|
|
8
|
+
|
|
9
|
+
# HTTP client instrumentation
|
|
10
|
+
if config.instrument_http
|
|
11
|
+
install_net_http!
|
|
12
|
+
install_faraday!
|
|
13
|
+
install_httparty!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Database instrumentation (breadcrumbs for Reflex)
|
|
17
|
+
install_active_record! if config.instrument_active_record
|
|
18
|
+
|
|
19
|
+
# Redis instrumentation
|
|
20
|
+
install_redis! if config.instrument_redis
|
|
21
|
+
|
|
22
|
+
# Background job instrumentation
|
|
23
|
+
install_sidekiq! if config.instrument_sidekiq
|
|
24
|
+
|
|
25
|
+
# GraphQL instrumentation
|
|
26
|
+
install_graphql! if config.instrument_graphql
|
|
27
|
+
|
|
28
|
+
# MongoDB instrumentation
|
|
29
|
+
install_mongodb! if config.instrument_mongodb
|
|
30
|
+
|
|
31
|
+
# Elasticsearch instrumentation
|
|
32
|
+
install_elasticsearch! if config.instrument_elasticsearch
|
|
33
|
+
|
|
34
|
+
# ActionMailer instrumentation
|
|
35
|
+
install_action_mailer! if config.instrument_action_mailer
|
|
36
|
+
|
|
37
|
+
# Delayed::Job instrumentation
|
|
38
|
+
install_delayed_job! if config.instrument_delayed_job
|
|
39
|
+
|
|
40
|
+
# Grape API instrumentation
|
|
41
|
+
install_grape! if config.instrument_grape
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def install_net_http!
|
|
45
|
+
require_relative "instrumentation/net_http"
|
|
46
|
+
NetHttp.install!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def install_faraday!
|
|
50
|
+
return unless defined?(::Faraday)
|
|
51
|
+
|
|
52
|
+
require_relative "instrumentation/faraday"
|
|
53
|
+
FaradayMiddleware.install!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def install_httparty!
|
|
57
|
+
return unless defined?(::HTTParty)
|
|
58
|
+
|
|
59
|
+
require_relative "instrumentation/httparty"
|
|
60
|
+
HTTPartyInstrumentation.install!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def install_active_record!
|
|
64
|
+
require_relative "instrumentation/active_record"
|
|
65
|
+
ActiveRecord.install!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def install_redis!
|
|
69
|
+
return unless defined?(::Redis)
|
|
70
|
+
|
|
71
|
+
require_relative "instrumentation/redis"
|
|
72
|
+
RedisInstrumentation.install!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def install_sidekiq!
|
|
76
|
+
return unless defined?(::Sidekiq)
|
|
77
|
+
|
|
78
|
+
require_relative "instrumentation/sidekiq"
|
|
79
|
+
SidekiqInstrumentation.install!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def install_graphql!
|
|
83
|
+
return unless defined?(::GraphQL)
|
|
84
|
+
|
|
85
|
+
require_relative "instrumentation/graphql"
|
|
86
|
+
GraphQLInstrumentation.install!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def install_mongodb!
|
|
90
|
+
return unless defined?(::Mongo) || defined?(::Mongoid)
|
|
91
|
+
|
|
92
|
+
require_relative "instrumentation/mongodb"
|
|
93
|
+
MongoDBInstrumentation.install!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def install_elasticsearch!
|
|
97
|
+
return unless defined?(::Elasticsearch) || defined?(::OpenSearch)
|
|
98
|
+
|
|
99
|
+
require_relative "instrumentation/elasticsearch"
|
|
100
|
+
ElasticsearchInstrumentation.install!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def install_action_mailer!
|
|
104
|
+
return unless defined?(::ActionMailer)
|
|
105
|
+
|
|
106
|
+
require_relative "instrumentation/action_mailer"
|
|
107
|
+
ActionMailerInstrumentation.install!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def install_delayed_job!
|
|
111
|
+
return unless defined?(::Delayed::Job) || defined?(::Delayed::Backend)
|
|
112
|
+
|
|
113
|
+
require_relative "instrumentation/delayed_job"
|
|
114
|
+
DelayedJobInstrumentation.install!
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def install_grape!
|
|
118
|
+
return unless defined?(::Grape::API)
|
|
119
|
+
|
|
120
|
+
require_relative "instrumentation/grape"
|
|
121
|
+
GrapeInstrumentation.install!
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Manual installation methods for lazy-loaded libraries
|
|
125
|
+
def install_http!
|
|
126
|
+
install_net_http!
|
|
127
|
+
install_faraday!
|
|
128
|
+
install_httparty!
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Pulse
|
|
9
|
+
class Client
|
|
10
|
+
MAX_RETRIES = 3
|
|
11
|
+
RETRY_DELAY = 0.5
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
@buffer = []
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@flush_thread = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def send_trace(payload)
|
|
21
|
+
return unless @config.pulse_enabled && @config.pulse_valid?
|
|
22
|
+
|
|
23
|
+
if @config.pulse_buffer_size > 1
|
|
24
|
+
buffer_trace(payload)
|
|
25
|
+
else
|
|
26
|
+
post("/api/v1/traces", payload)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def send_batch(payloads)
|
|
31
|
+
return unless @config.pulse_enabled && @config.pulse_valid?
|
|
32
|
+
return if payloads.empty?
|
|
33
|
+
|
|
34
|
+
post("/api/v1/traces/batch", { traces: payloads })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def send_metric(payload)
|
|
38
|
+
return unless @config.pulse_enabled && @config.pulse_valid?
|
|
39
|
+
|
|
40
|
+
post("/api/v1/metrics", payload)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def flush
|
|
44
|
+
traces_to_send = nil
|
|
45
|
+
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
return if @buffer.empty?
|
|
48
|
+
|
|
49
|
+
traces_to_send = @buffer.dup
|
|
50
|
+
@buffer.clear
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
send_batch(traces_to_send) if traces_to_send&.any?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def buffer_trace(payload)
|
|
59
|
+
should_flush = false
|
|
60
|
+
|
|
61
|
+
@mutex.synchronize do
|
|
62
|
+
@buffer << payload
|
|
63
|
+
should_flush = @buffer.size >= @config.pulse_buffer_size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
start_flush_timer unless @flush_thread&.alive?
|
|
67
|
+
flush if should_flush
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def start_flush_timer
|
|
71
|
+
@flush_thread = Thread.new do
|
|
72
|
+
loop do
|
|
73
|
+
sleep(@config.pulse_flush_interval)
|
|
74
|
+
flush
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def post(path, body)
|
|
80
|
+
uri = URI.join(@config.pulse_url, path)
|
|
81
|
+
request = Net::HTTP::Post.new(uri)
|
|
82
|
+
request["Content-Type"] = "application/json"
|
|
83
|
+
request["Authorization"] = "Bearer #{@config.pulse_auth_key}"
|
|
84
|
+
request["User-Agent"] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
85
|
+
request.body = JSON.generate(body)
|
|
86
|
+
|
|
87
|
+
execute_with_retry(uri, request)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
log_error("Failed to send to Pulse: #{e.message}")
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def execute_with_retry(uri, request)
|
|
94
|
+
retries = 0
|
|
95
|
+
begin
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.use_ssl = uri.scheme == "https"
|
|
98
|
+
http.open_timeout = 5
|
|
99
|
+
http.read_timeout = 10
|
|
100
|
+
|
|
101
|
+
response = http.request(request)
|
|
102
|
+
|
|
103
|
+
case response.code.to_i
|
|
104
|
+
when 200..299
|
|
105
|
+
JSON.parse(response.body) rescue {}
|
|
106
|
+
when 429, 500..599
|
|
107
|
+
raise RetryableError, "Server error: #{response.code}"
|
|
108
|
+
else
|
|
109
|
+
log_error("Pulse API error: #{response.code} - #{response.body}")
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
113
|
+
retries += 1
|
|
114
|
+
if retries <= MAX_RETRIES
|
|
115
|
+
sleep(RETRY_DELAY * retries)
|
|
116
|
+
retry
|
|
117
|
+
end
|
|
118
|
+
log_error("Failed after #{MAX_RETRIES} retries: #{e.message}")
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def log_error(message)
|
|
124
|
+
return unless @config.logger
|
|
125
|
+
|
|
126
|
+
@config.logger.error("[BrainzLab::Pulse] #{message}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class RetryableError < StandardError; end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Pulse
|
|
5
|
+
class Instrumentation
|
|
6
|
+
class << self
|
|
7
|
+
def install!
|
|
8
|
+
return unless BrainzLab.configuration.pulse_enabled
|
|
9
|
+
|
|
10
|
+
install_active_record!
|
|
11
|
+
install_action_view!
|
|
12
|
+
install_active_support_cache!
|
|
13
|
+
install_action_controller!
|
|
14
|
+
install_http_clients!
|
|
15
|
+
install_active_job!
|
|
16
|
+
install_action_cable!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Track SQL queries
|
|
22
|
+
def install_active_record!
|
|
23
|
+
return unless defined?(ActiveRecord)
|
|
24
|
+
|
|
25
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
26
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
27
|
+
next if skip_query?(event.payload)
|
|
28
|
+
|
|
29
|
+
record_span(
|
|
30
|
+
name: event.payload[:name] || "SQL",
|
|
31
|
+
kind: "db",
|
|
32
|
+
started_at: event.time,
|
|
33
|
+
ended_at: event.end,
|
|
34
|
+
duration_ms: event.duration,
|
|
35
|
+
data: {
|
|
36
|
+
sql: truncate_sql(event.payload[:sql]),
|
|
37
|
+
name: event.payload[:name],
|
|
38
|
+
cached: event.payload[:cached] || false
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Track view rendering
|
|
45
|
+
def install_action_view!
|
|
46
|
+
return unless defined?(ActionView)
|
|
47
|
+
|
|
48
|
+
ActiveSupport::Notifications.subscribe("render_template.action_view") do |*args|
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
|
|
51
|
+
record_span(
|
|
52
|
+
name: short_path(event.payload[:identifier]),
|
|
53
|
+
kind: "render",
|
|
54
|
+
started_at: event.time,
|
|
55
|
+
ended_at: event.end,
|
|
56
|
+
duration_ms: event.duration,
|
|
57
|
+
data: {
|
|
58
|
+
identifier: event.payload[:identifier],
|
|
59
|
+
layout: event.payload[:layout]
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
ActiveSupport::Notifications.subscribe("render_partial.action_view") do |*args|
|
|
65
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
66
|
+
|
|
67
|
+
record_span(
|
|
68
|
+
name: short_path(event.payload[:identifier]),
|
|
69
|
+
kind: "render",
|
|
70
|
+
started_at: event.time,
|
|
71
|
+
ended_at: event.end,
|
|
72
|
+
duration_ms: event.duration,
|
|
73
|
+
data: {
|
|
74
|
+
identifier: event.payload[:identifier],
|
|
75
|
+
partial: true
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
ActiveSupport::Notifications.subscribe("render_collection.action_view") do |*args|
|
|
81
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
82
|
+
|
|
83
|
+
record_span(
|
|
84
|
+
name: short_path(event.payload[:identifier]),
|
|
85
|
+
kind: "render",
|
|
86
|
+
started_at: event.time,
|
|
87
|
+
ended_at: event.end,
|
|
88
|
+
duration_ms: event.duration,
|
|
89
|
+
data: {
|
|
90
|
+
identifier: event.payload[:identifier],
|
|
91
|
+
count: event.payload[:count],
|
|
92
|
+
collection: true
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Track cache operations
|
|
99
|
+
def install_active_support_cache!
|
|
100
|
+
%w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
|
|
101
|
+
ActiveSupport::Notifications.subscribe(event_name) do |*args|
|
|
102
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
103
|
+
operation = event_name.split(".").first.sub("cache_", "")
|
|
104
|
+
|
|
105
|
+
record_span(
|
|
106
|
+
name: "Cache #{operation}",
|
|
107
|
+
kind: "cache",
|
|
108
|
+
started_at: event.time,
|
|
109
|
+
ended_at: event.end,
|
|
110
|
+
duration_ms: event.duration,
|
|
111
|
+
data: {
|
|
112
|
+
key: truncate_key(event.payload[:key]),
|
|
113
|
+
hit: event.payload[:hit],
|
|
114
|
+
operation: operation
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Track controller processing for timing breakdown
|
|
122
|
+
def install_action_controller!
|
|
123
|
+
return unless defined?(ActionController)
|
|
124
|
+
|
|
125
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
126
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
127
|
+
payload = event.payload
|
|
128
|
+
|
|
129
|
+
# Store timing breakdown in thread local for the middleware
|
|
130
|
+
Thread.current[:brainzlab_pulse_breakdown] = {
|
|
131
|
+
view_ms: payload[:view_runtime]&.round(2),
|
|
132
|
+
db_ms: payload[:db_runtime]&.round(2)
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Track external HTTP requests
|
|
138
|
+
def install_http_clients!
|
|
139
|
+
# Net::HTTP instrumentation
|
|
140
|
+
if defined?(Net::HTTP)
|
|
141
|
+
ActiveSupport::Notifications.subscribe("request.net_http") do |*args|
|
|
142
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
143
|
+
|
|
144
|
+
record_span(
|
|
145
|
+
name: "HTTP #{event.payload[:method]} #{event.payload[:host]}",
|
|
146
|
+
kind: "http",
|
|
147
|
+
started_at: event.time,
|
|
148
|
+
ended_at: event.end,
|
|
149
|
+
duration_ms: event.duration,
|
|
150
|
+
data: {
|
|
151
|
+
method: event.payload[:method],
|
|
152
|
+
host: event.payload[:host],
|
|
153
|
+
path: event.payload[:path],
|
|
154
|
+
status: event.payload[:code]
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Faraday instrumentation
|
|
161
|
+
if defined?(Faraday)
|
|
162
|
+
ActiveSupport::Notifications.subscribe("request.faraday") do |*args|
|
|
163
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
164
|
+
env = event.payload[:env]
|
|
165
|
+
next unless env
|
|
166
|
+
|
|
167
|
+
record_span(
|
|
168
|
+
name: "HTTP #{env.method.to_s.upcase} #{env.url.host}",
|
|
169
|
+
kind: "http",
|
|
170
|
+
started_at: event.time,
|
|
171
|
+
ended_at: event.end,
|
|
172
|
+
duration_ms: event.duration,
|
|
173
|
+
data: {
|
|
174
|
+
method: env.method.to_s.upcase,
|
|
175
|
+
host: env.url.host,
|
|
176
|
+
path: env.url.path,
|
|
177
|
+
status: env.status
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Track ActiveJob/SolidQueue
|
|
185
|
+
def install_active_job!
|
|
186
|
+
return unless defined?(ActiveJob)
|
|
187
|
+
|
|
188
|
+
# Track job enqueuing
|
|
189
|
+
ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
|
|
190
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
191
|
+
job = event.payload[:job]
|
|
192
|
+
|
|
193
|
+
record_span(
|
|
194
|
+
name: "Enqueue #{job.class.name}",
|
|
195
|
+
kind: "job",
|
|
196
|
+
started_at: event.time,
|
|
197
|
+
ended_at: event.end,
|
|
198
|
+
duration_ms: event.duration,
|
|
199
|
+
data: {
|
|
200
|
+
job_class: job.class.name,
|
|
201
|
+
job_id: job.job_id,
|
|
202
|
+
queue: job.queue_name
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Track job retry
|
|
208
|
+
ActiveSupport::Notifications.subscribe("retry_stopped.active_job") do |*args|
|
|
209
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
210
|
+
job = event.payload[:job]
|
|
211
|
+
error = event.payload[:error]
|
|
212
|
+
|
|
213
|
+
record_span(
|
|
214
|
+
name: "Retry stopped #{job.class.name}",
|
|
215
|
+
kind: "job",
|
|
216
|
+
started_at: event.time,
|
|
217
|
+
ended_at: event.end,
|
|
218
|
+
duration_ms: event.duration,
|
|
219
|
+
error: true,
|
|
220
|
+
error_class: error&.class&.name,
|
|
221
|
+
error_message: error&.message,
|
|
222
|
+
data: {
|
|
223
|
+
job_class: job.class.name,
|
|
224
|
+
job_id: job.job_id,
|
|
225
|
+
queue: job.queue_name,
|
|
226
|
+
executions: job.executions
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Track job discard
|
|
232
|
+
ActiveSupport::Notifications.subscribe("discard.active_job") do |*args|
|
|
233
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
234
|
+
job = event.payload[:job]
|
|
235
|
+
error = event.payload[:error]
|
|
236
|
+
|
|
237
|
+
record_span(
|
|
238
|
+
name: "Discarded #{job.class.name}",
|
|
239
|
+
kind: "job",
|
|
240
|
+
started_at: event.time,
|
|
241
|
+
ended_at: event.end,
|
|
242
|
+
duration_ms: event.duration,
|
|
243
|
+
error: true,
|
|
244
|
+
error_class: error&.class&.name,
|
|
245
|
+
error_message: error&.message,
|
|
246
|
+
data: {
|
|
247
|
+
job_class: job.class.name,
|
|
248
|
+
job_id: job.job_id,
|
|
249
|
+
queue: job.queue_name,
|
|
250
|
+
executions: job.executions
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Track ActionCable/SolidCable
|
|
257
|
+
def install_action_cable!
|
|
258
|
+
return unless defined?(ActionCable)
|
|
259
|
+
|
|
260
|
+
ActiveSupport::Notifications.subscribe("perform_action.action_cable") do |*args|
|
|
261
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
262
|
+
|
|
263
|
+
record_span(
|
|
264
|
+
name: "Cable #{event.payload[:channel_class]}##{event.payload[:action]}",
|
|
265
|
+
kind: "cable",
|
|
266
|
+
started_at: event.time,
|
|
267
|
+
ended_at: event.end,
|
|
268
|
+
duration_ms: event.duration,
|
|
269
|
+
data: {
|
|
270
|
+
channel: event.payload[:channel_class],
|
|
271
|
+
action: event.payload[:action]
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
ActiveSupport::Notifications.subscribe("transmit.action_cable") do |*args|
|
|
277
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
278
|
+
|
|
279
|
+
record_span(
|
|
280
|
+
name: "Cable transmit #{event.payload[:channel_class]}",
|
|
281
|
+
kind: "cable",
|
|
282
|
+
started_at: event.time,
|
|
283
|
+
ended_at: event.end,
|
|
284
|
+
duration_ms: event.duration,
|
|
285
|
+
data: {
|
|
286
|
+
channel: event.payload[:channel_class],
|
|
287
|
+
via: event.payload[:via]
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
ActiveSupport::Notifications.subscribe("broadcast.action_cable") do |*args|
|
|
293
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
294
|
+
|
|
295
|
+
record_span(
|
|
296
|
+
name: "Cable broadcast #{event.payload[:broadcasting]}",
|
|
297
|
+
kind: "cable",
|
|
298
|
+
started_at: event.time,
|
|
299
|
+
ended_at: event.end,
|
|
300
|
+
duration_ms: event.duration,
|
|
301
|
+
data: {
|
|
302
|
+
broadcasting: event.payload[:broadcasting],
|
|
303
|
+
coder: event.payload[:coder]
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def record_span(name:, kind:, started_at:, ended_at:, duration_ms:, error: false, error_class: nil, error_message: nil, data: {})
|
|
310
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
311
|
+
return unless spans
|
|
312
|
+
|
|
313
|
+
span = {
|
|
314
|
+
span_id: SecureRandom.uuid,
|
|
315
|
+
name: name,
|
|
316
|
+
kind: kind,
|
|
317
|
+
started_at: started_at,
|
|
318
|
+
ended_at: ended_at,
|
|
319
|
+
duration_ms: duration_ms.round(2),
|
|
320
|
+
data: data.compact
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if error
|
|
324
|
+
span[:error] = true
|
|
325
|
+
span[:error_class] = error_class
|
|
326
|
+
span[:error_message] = error_message
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
spans << span
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def skip_query?(payload)
|
|
333
|
+
# Skip SCHEMA queries and internal Rails queries
|
|
334
|
+
return true if payload[:name] == "SCHEMA"
|
|
335
|
+
return true if payload[:name]&.start_with?("EXPLAIN")
|
|
336
|
+
return true if payload[:sql]&.include?("pg_")
|
|
337
|
+
return true if payload[:sql]&.include?("information_schema")
|
|
338
|
+
return true if payload[:cached] && !include_cached_queries?
|
|
339
|
+
|
|
340
|
+
false
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def include_cached_queries?
|
|
344
|
+
false
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def truncate_sql(sql)
|
|
348
|
+
return nil unless sql
|
|
349
|
+
sql.to_s[0, 1000]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def truncate_key(key)
|
|
353
|
+
return nil unless key
|
|
354
|
+
key.to_s[0, 200]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def short_path(path)
|
|
358
|
+
return nil unless path
|
|
359
|
+
path.to_s.split("/").last(2).join("/")
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|