dontbugme 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/LICENSE +21 -0
- data/README.md +174 -0
- data/app/controllers/dontbugme/traces_controller.rb +45 -0
- data/app/views/dontbugme/traces/diff.html.erb +18 -0
- data/app/views/dontbugme/traces/index.html.erb +30 -0
- data/app/views/dontbugme/traces/show.html.erb +15 -0
- data/app/views/layouts/dontbugme/application.html.erb +56 -0
- data/bin/dontbugme +5 -0
- data/lib/dontbugme/cleanup_job.rb +17 -0
- data/lib/dontbugme/cli.rb +171 -0
- data/lib/dontbugme/config/routes.rb +7 -0
- data/lib/dontbugme/configuration.rb +147 -0
- data/lib/dontbugme/context.rb +25 -0
- data/lib/dontbugme/correlation.rb +25 -0
- data/lib/dontbugme/engine.rb +11 -0
- data/lib/dontbugme/formatters/diff.rb +187 -0
- data/lib/dontbugme/formatters/json.rb +11 -0
- data/lib/dontbugme/formatters/timeline.rb +119 -0
- data/lib/dontbugme/middleware/rack.rb +37 -0
- data/lib/dontbugme/middleware/sidekiq.rb +31 -0
- data/lib/dontbugme/middleware/sidekiq_client.rb +14 -0
- data/lib/dontbugme/railtie.rb +47 -0
- data/lib/dontbugme/recorder.rb +70 -0
- data/lib/dontbugme/source_location.rb +44 -0
- data/lib/dontbugme/span.rb +70 -0
- data/lib/dontbugme/span_collection.rb +40 -0
- data/lib/dontbugme/store/async.rb +45 -0
- data/lib/dontbugme/store/base.rb +23 -0
- data/lib/dontbugme/store/memory.rb +61 -0
- data/lib/dontbugme/store/postgresql.rb +186 -0
- data/lib/dontbugme/store/sqlite.rb +148 -0
- data/lib/dontbugme/subscribers/action_mailer.rb +53 -0
- data/lib/dontbugme/subscribers/active_job.rb +44 -0
- data/lib/dontbugme/subscribers/active_record.rb +81 -0
- data/lib/dontbugme/subscribers/base.rb +19 -0
- data/lib/dontbugme/subscribers/cache.rb +54 -0
- data/lib/dontbugme/subscribers/net_http.rb +87 -0
- data/lib/dontbugme/subscribers/redis.rb +63 -0
- data/lib/dontbugme/trace.rb +142 -0
- data/lib/dontbugme/version.rb +5 -0
- data/lib/dontbugme.rb +118 -0
- data/lib/generators/dontbugme/install/install_generator.rb +17 -0
- data/lib/generators/dontbugme/install/templates/dontbugme.rb +17 -0
- metadata +164 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class ActionMailer
|
|
6
|
+
EVENT = 'deliver.action_mailer'
|
|
7
|
+
|
|
8
|
+
def self.subscribe
|
|
9
|
+
return unless defined?(::ActionMailer::Base)
|
|
10
|
+
|
|
11
|
+
::ActiveSupport::Notifications.subscribe(EVENT) do |*args|
|
|
12
|
+
call(*args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(_name, start, finish, _id, payload)
|
|
17
|
+
return unless Context.active?
|
|
18
|
+
return unless Dontbugme.config.recording?
|
|
19
|
+
|
|
20
|
+
duration_ms = ((finish - start) * 1000).round(2)
|
|
21
|
+
mailer = payload[:mailer] || payload['mailer']
|
|
22
|
+
action = payload[:action] || payload['action']
|
|
23
|
+
message_id = payload[:message_id] || payload['message_id']
|
|
24
|
+
|
|
25
|
+
detail = "#{mailer}##{action}"
|
|
26
|
+
payload_data = {
|
|
27
|
+
mailer: mailer,
|
|
28
|
+
action: action,
|
|
29
|
+
message_id: message_id
|
|
30
|
+
}
|
|
31
|
+
payload_data[:to] = payload[:to] if payload[:to]
|
|
32
|
+
payload_data[:subject] = truncate(payload[:subject].to_s, 100) if payload[:subject]
|
|
33
|
+
|
|
34
|
+
Recorder.add_span(
|
|
35
|
+
category: :mailer,
|
|
36
|
+
operation: 'deliver',
|
|
37
|
+
detail: detail,
|
|
38
|
+
payload: payload_data,
|
|
39
|
+
duration_ms: duration_ms,
|
|
40
|
+
started_at: start
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def truncate(str, max)
|
|
47
|
+
return str if str.length <= max
|
|
48
|
+
|
|
49
|
+
"#{str[0, max]}..."
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class ActiveJob
|
|
6
|
+
EVENT = 'enqueue.active_job'
|
|
7
|
+
|
|
8
|
+
def self.subscribe
|
|
9
|
+
return unless defined?(::ActiveJob::Base)
|
|
10
|
+
|
|
11
|
+
::ActiveSupport::Notifications.subscribe(EVENT) do |*args|
|
|
12
|
+
call(*args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(_name, start, finish, _id, payload)
|
|
17
|
+
return unless Context.active?
|
|
18
|
+
return unless Dontbugme.config.recording?
|
|
19
|
+
|
|
20
|
+
duration_ms = ((finish - start) * 1000).round(2)
|
|
21
|
+
job_class = payload[:job]&.class&.name || payload[:job_class] || payload['job_class']
|
|
22
|
+
queue = payload[:queue] || payload['queue']
|
|
23
|
+
args = payload[:args] || payload['args'] || []
|
|
24
|
+
|
|
25
|
+
detail = "ENQUEUE #{job_class}"
|
|
26
|
+
payload_data = {
|
|
27
|
+
job: job_class,
|
|
28
|
+
queue: queue,
|
|
29
|
+
args: args.is_a?(Array) ? args.first(5) : args
|
|
30
|
+
}
|
|
31
|
+
payload_data[:scheduled_at] = payload[:scheduled_at] if payload[:scheduled_at]
|
|
32
|
+
|
|
33
|
+
Recorder.add_span(
|
|
34
|
+
category: :enqueue,
|
|
35
|
+
operation: 'ENQUEUE',
|
|
36
|
+
detail: detail,
|
|
37
|
+
payload: payload_data,
|
|
38
|
+
duration_ms: duration_ms,
|
|
39
|
+
started_at: start
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class ActiveRecord < Base
|
|
6
|
+
EVENT = 'sql.active_record'
|
|
7
|
+
|
|
8
|
+
def self.subscribe
|
|
9
|
+
return unless defined?(::ActiveRecord::Base)
|
|
10
|
+
|
|
11
|
+
::ActiveSupport::Notifications.subscribe(EVENT) do |*args|
|
|
12
|
+
call(*args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(_name, start, finish, _id, payload)
|
|
17
|
+
return unless Context.active?
|
|
18
|
+
|
|
19
|
+
config = Dontbugme.config
|
|
20
|
+
return unless config.recording?
|
|
21
|
+
|
|
22
|
+
duration_ms = ((finish - start) * 1000).round(2)
|
|
23
|
+
sql = payload[:sql] || payload['sql'] || ''
|
|
24
|
+
binds = config.capture_sql_binds ? process_binds(payload) : []
|
|
25
|
+
|
|
26
|
+
operation = extract_operation(sql)
|
|
27
|
+
payload_data = {
|
|
28
|
+
name: payload[:name] || payload['name'],
|
|
29
|
+
connection_id: payload[:connection_id] || payload['connection_id']
|
|
30
|
+
}
|
|
31
|
+
payload_data[:binds] = binds if binds.any?
|
|
32
|
+
|
|
33
|
+
Recorder.add_span(
|
|
34
|
+
category: :sql,
|
|
35
|
+
operation: operation,
|
|
36
|
+
detail: sql,
|
|
37
|
+
payload: payload_data,
|
|
38
|
+
duration_ms: duration_ms,
|
|
39
|
+
started_at: start
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_operation(sql)
|
|
46
|
+
return 'UNKNOWN' if sql.nil? || sql.empty?
|
|
47
|
+
|
|
48
|
+
sql = sql.strip.upcase
|
|
49
|
+
if sql.start_with?('SELECT') then 'SELECT'
|
|
50
|
+
elsif sql.start_with?('INSERT') then 'INSERT'
|
|
51
|
+
elsif sql.start_with?('UPDATE') then 'UPDATE'
|
|
52
|
+
elsif sql.start_with?('DELETE') then 'DELETE'
|
|
53
|
+
elsif sql.start_with?('BEGIN') then 'BEGIN'
|
|
54
|
+
elsif sql.start_with?('COMMIT') then 'COMMIT'
|
|
55
|
+
elsif sql.start_with?('ROLLBACK') then 'ROLLBACK'
|
|
56
|
+
elsif sql.start_with?('SAVEPOINT') then 'SAVEPOINT'
|
|
57
|
+
elsif sql.start_with?('RELEASE') then 'RELEASE'
|
|
58
|
+
else 'OTHER'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_binds(payload)
|
|
63
|
+
binds = payload[:binds] || payload['binds'] || []
|
|
64
|
+
type_casted = payload[:type_casted_binds] || payload['type_casted_binds'] || binds
|
|
65
|
+
max_size = Dontbugme.config.max_sql_bind_size
|
|
66
|
+
|
|
67
|
+
Array(type_casted).map do |val|
|
|
68
|
+
truncate_value(val, max_size)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def truncate_value(val, max_size)
|
|
73
|
+
str = val.to_s
|
|
74
|
+
return str if str.bytesize <= max_size
|
|
75
|
+
|
|
76
|
+
truncated = str.byteslice(0, max_size)
|
|
77
|
+
"#{truncated}[truncated, #{str.bytesize}B]"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class Base
|
|
6
|
+
def self.subscribe
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.call(*args)
|
|
11
|
+
new.call(*args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(_name, _start, _finish, _id, _payload)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class Cache
|
|
6
|
+
EVENTS = %w[
|
|
7
|
+
cache_read.active_support
|
|
8
|
+
cache_write.active_support
|
|
9
|
+
cache_delete.active_support
|
|
10
|
+
cache_exist?.active_support
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def self.subscribe
|
|
14
|
+
return unless defined?(ActiveSupport::Cache::Store)
|
|
15
|
+
|
|
16
|
+
EVENTS.each do |event|
|
|
17
|
+
::ActiveSupport::Notifications.subscribe(event) do |*args|
|
|
18
|
+
new.call(*args)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(name, start, finish, _id, payload)
|
|
24
|
+
return unless Context.active?
|
|
25
|
+
return unless Dontbugme.config.recording?
|
|
26
|
+
|
|
27
|
+
duration_ms = ((finish - start) * 1000).round(2)
|
|
28
|
+
operation = payload[:operation] || payload['operation'] || extract_operation(name)
|
|
29
|
+
key = payload[:key] || payload['key']
|
|
30
|
+
hit = payload[:hit] if payload.key?(:hit) || payload.key?('hit')
|
|
31
|
+
|
|
32
|
+
detail = "cache #{operation} #{key}"
|
|
33
|
+
payload_data = { key: key }
|
|
34
|
+
payload_data[:hit] = hit unless hit.nil?
|
|
35
|
+
payload_data[:super_operation] = payload[:super_operation] if payload[:super_operation]
|
|
36
|
+
|
|
37
|
+
Recorder.add_span(
|
|
38
|
+
category: :cache,
|
|
39
|
+
operation: operation.to_s.upcase,
|
|
40
|
+
detail: detail,
|
|
41
|
+
payload: payload_data,
|
|
42
|
+
duration_ms: duration_ms,
|
|
43
|
+
started_at: start
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def extract_operation(event_name)
|
|
50
|
+
event_name.to_s.split('.').first
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class NetHttp
|
|
6
|
+
def self.subscribe
|
|
7
|
+
return unless defined?(Net::HTTP)
|
|
8
|
+
|
|
9
|
+
Net::HTTP.prepend(Instrumentation)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Instrumentation
|
|
13
|
+
def request(req, body = nil, &block)
|
|
14
|
+
return super unless Dontbugme::Context.active?
|
|
15
|
+
return super unless Dontbugme.config.recording?
|
|
16
|
+
|
|
17
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
18
|
+
start_wall = Time.now
|
|
19
|
+
response = super
|
|
20
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
21
|
+
|
|
22
|
+
record_span(req, response, start_wall, duration_ms)
|
|
23
|
+
response
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
26
|
+
record_span(req, nil, start_wall, duration_ms, error: e)
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def record_span(req, response, start_wall, duration_ms, error: nil)
|
|
33
|
+
config = Dontbugme.config
|
|
34
|
+
uri = build_uri(req)
|
|
35
|
+
method = req.method
|
|
36
|
+
detail = "#{method} #{uri}"
|
|
37
|
+
payload = {
|
|
38
|
+
method: method,
|
|
39
|
+
url: uri,
|
|
40
|
+
status: response&.code&.to_i
|
|
41
|
+
}
|
|
42
|
+
payload[:error] = error.message if error
|
|
43
|
+
|
|
44
|
+
if config.capture_http_headers&.any?
|
|
45
|
+
payload[:request_headers] = capture_headers(req, config.capture_http_headers)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Dontbugme::Recorder.add_span(
|
|
49
|
+
category: :http,
|
|
50
|
+
operation: method,
|
|
51
|
+
detail: detail,
|
|
52
|
+
payload: payload,
|
|
53
|
+
duration_ms: duration_ms,
|
|
54
|
+
started_at: start_wall
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_uri(req)
|
|
59
|
+
path = req.path.to_s.empty? ? '/' : req.path
|
|
60
|
+
"#{use_ssl? ? 'https' : 'http'}://#{address}:#{port}#{path}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def capture_headers(req, header_names)
|
|
64
|
+
result = {}
|
|
65
|
+
max = Dontbugme.config.max_http_body_size
|
|
66
|
+
header_map = {
|
|
67
|
+
'content_type' => 'Content-Type',
|
|
68
|
+
'authorization_type' => 'Authorization',
|
|
69
|
+
'authorization' => 'Authorization'
|
|
70
|
+
}
|
|
71
|
+
header_names.each do |name|
|
|
72
|
+
key = header_map[name.to_s.downcase] || name.to_s.split('_').map(&:capitalize).join('-')
|
|
73
|
+
val = req[key]
|
|
74
|
+
result[name.to_s] = truncate(val.to_s, max) if val
|
|
75
|
+
end
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def truncate(str, max)
|
|
80
|
+
return str if str.bytesize <= max
|
|
81
|
+
|
|
82
|
+
"#{str.byteslice(0, max)}[truncated]"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Subscribers
|
|
5
|
+
class Redis
|
|
6
|
+
def self.subscribe
|
|
7
|
+
return unless defined?(::Redis)
|
|
8
|
+
return unless defined?(::Redis::Client)
|
|
9
|
+
|
|
10
|
+
::Redis::Client.prepend(Instrumentation)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Instrumentation
|
|
14
|
+
def call(command, &block)
|
|
15
|
+
return super unless Dontbugme::Context.active?
|
|
16
|
+
return super unless Dontbugme.config.recording?
|
|
17
|
+
|
|
18
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
19
|
+
start_wall = Time.now
|
|
20
|
+
result = super
|
|
21
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
22
|
+
|
|
23
|
+
record_span(command, start_wall, duration_ms)
|
|
24
|
+
result
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round(2)
|
|
27
|
+
record_span(command, start_wall, duration_ms, error: e)
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def record_span(command, start_wall, duration_ms, error: nil)
|
|
34
|
+
cmd = Array(command).map(&:to_s)
|
|
35
|
+
operation = cmd.first&.upcase || 'UNKNOWN'
|
|
36
|
+
detail = cmd.join(' ')
|
|
37
|
+
config = Dontbugme.config
|
|
38
|
+
|
|
39
|
+
payload = { command: operation }
|
|
40
|
+
if config.capture_redis_values && cmd.size > 1
|
|
41
|
+
payload[:args] = cmd[1..].map { |a| truncate(a, config.max_redis_value_size) }
|
|
42
|
+
end
|
|
43
|
+
payload[:error] = error.message if error
|
|
44
|
+
|
|
45
|
+
Dontbugme::Recorder.add_span(
|
|
46
|
+
category: :redis,
|
|
47
|
+
operation: operation,
|
|
48
|
+
detail: detail,
|
|
49
|
+
payload: payload,
|
|
50
|
+
duration_ms: duration_ms,
|
|
51
|
+
started_at: start_wall
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def truncate(str, max)
|
|
56
|
+
return str if str.to_s.bytesize <= max
|
|
57
|
+
|
|
58
|
+
"#{str.to_s.byteslice(0, max)}[truncated]"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Dontbugme
|
|
6
|
+
class Trace
|
|
7
|
+
attr_reader :id, :kind, :identifier, :metadata, :correlation_id
|
|
8
|
+
attr_accessor :status, :error, :truncated_spans_count
|
|
9
|
+
|
|
10
|
+
def initialize(kind:, identifier:, metadata: {})
|
|
11
|
+
@id = "tr_#{SecureRandom.hex(6)}"
|
|
12
|
+
@kind = kind.to_sym
|
|
13
|
+
@identifier = identifier.to_s
|
|
14
|
+
@metadata = metadata
|
|
15
|
+
@correlation_id = metadata[:correlation_id] || metadata['correlation_id']
|
|
16
|
+
@spans = []
|
|
17
|
+
@started_at_utc = Time.now.utc
|
|
18
|
+
@started_at_monotonic = now_monotonic_ms
|
|
19
|
+
@finished_at = nil
|
|
20
|
+
@status = :success
|
|
21
|
+
@error = nil
|
|
22
|
+
@truncated_spans_count = 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def merge_tags!(**tags)
|
|
26
|
+
@metadata.merge!(tags.transform_keys(&:to_sym))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_span(span)
|
|
30
|
+
config = Dontbugme.config
|
|
31
|
+
max_spans = config.max_spans_per_trace
|
|
32
|
+
|
|
33
|
+
if @spans.size >= max_spans
|
|
34
|
+
@truncated_spans_count += 1
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@spans << span
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def spans
|
|
42
|
+
@span_collection ||= SpanCollection.new(@spans)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def raw_spans
|
|
46
|
+
@spans.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def finish!(error: nil)
|
|
50
|
+
@finished_at = now_monotonic_ms
|
|
51
|
+
@status = error ? :error : :success
|
|
52
|
+
@error = error ? format_error(error) : nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def started_at_utc
|
|
56
|
+
@started_at_utc
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Monotonic start time, used for computing span offsets during recording
|
|
60
|
+
def started_at_monotonic
|
|
61
|
+
@started_at_monotonic
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Wall-clock start time for computing span offsets from ActiveSupport::Notifications
|
|
65
|
+
def started_at_time
|
|
66
|
+
@started_at_utc
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def duration_ms
|
|
70
|
+
return @duration_ms_stored if defined?(@duration_ms_stored) && @duration_ms_stored
|
|
71
|
+
return nil unless @finished_at
|
|
72
|
+
|
|
73
|
+
(@finished_at - @started_at_monotonic).round(2)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{
|
|
78
|
+
id: id,
|
|
79
|
+
kind: kind,
|
|
80
|
+
identifier: identifier,
|
|
81
|
+
started_at: format_time(started_at_utc),
|
|
82
|
+
finished_at: @finished_at ? format_time(Time.at(@finished_at / 1000.0).utc) : nil,
|
|
83
|
+
duration_ms: duration_ms,
|
|
84
|
+
status: status,
|
|
85
|
+
error: error,
|
|
86
|
+
metadata: metadata,
|
|
87
|
+
correlation_id: correlation_id,
|
|
88
|
+
spans: raw_spans.map(&:to_h),
|
|
89
|
+
truncated_spans_count: truncated_spans_count
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.from_h(hash)
|
|
94
|
+
trace = allocate
|
|
95
|
+
trace.instance_variable_set(:@id, hash[:id] || hash['id'])
|
|
96
|
+
trace.instance_variable_set(:@kind, (hash[:kind] || hash['kind']).to_sym)
|
|
97
|
+
trace.instance_variable_set(:@identifier, hash[:identifier] || hash['identifier'])
|
|
98
|
+
trace.instance_variable_set(:@metadata, (hash[:metadata] || hash['metadata'] || {}).transform_keys(&:to_sym))
|
|
99
|
+
trace.instance_variable_set(:@correlation_id, hash[:correlation_id] || hash['correlation_id'])
|
|
100
|
+
trace.instance_variable_set(:@status, (hash[:status] || hash['status'] || :success).to_sym)
|
|
101
|
+
trace.instance_variable_set(:@error, hash[:error] || hash['error'])
|
|
102
|
+
trace.instance_variable_set(:@truncated_spans_count, hash[:truncated_spans_count] || hash['truncated_spans_count'] || 0)
|
|
103
|
+
|
|
104
|
+
spans_data = hash[:spans] || hash['spans'] || []
|
|
105
|
+
trace.instance_variable_set(:@spans, spans_data.map { |s| Span.from_h(s) })
|
|
106
|
+
|
|
107
|
+
started = hash[:started_at] || hash['started_at']
|
|
108
|
+
trace.instance_variable_set(:@started_at_utc, started ? Time.parse(started.to_s) : nil)
|
|
109
|
+
trace.instance_variable_set(:@started_at_monotonic, 0)
|
|
110
|
+
trace.instance_variable_set(:@finished_at, nil)
|
|
111
|
+
trace.instance_variable_set(:@duration_ms_stored, hash[:duration_ms] || hash['duration_ms'])
|
|
112
|
+
trace
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Convenience for trace.spans by category
|
|
116
|
+
def spans_by_category(cat)
|
|
117
|
+
spans.select { |s| s.category == cat.to_sym }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_timeline(only: nil, slow: nil)
|
|
121
|
+
Formatters::Timeline.format(self, only: only, slow: slow)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def now_monotonic_ms
|
|
127
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_time(t)
|
|
131
|
+
t.respond_to?(:iso8601) ? t.iso8601(3) : t.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def format_error(err)
|
|
135
|
+
{
|
|
136
|
+
class: err.class.name,
|
|
137
|
+
message: err.message,
|
|
138
|
+
backtrace: err.backtrace&.first(20)
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/dontbugme.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Dontbugme
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :config, :store
|
|
10
|
+
|
|
11
|
+
def config
|
|
12
|
+
@config ||= Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure
|
|
16
|
+
yield config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def store
|
|
20
|
+
@store ||= build_store
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def trace(identifier, metadata: {}, &block)
|
|
24
|
+
Recorder.record(kind: :custom, identifier: identifier, metadata: metadata, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def span(name, payload: {}, &block)
|
|
28
|
+
return yield unless Context.active?
|
|
29
|
+
|
|
30
|
+
start_mono = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
31
|
+
start_wall = Time.now
|
|
32
|
+
result = yield
|
|
33
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_mono).round(2)
|
|
34
|
+
|
|
35
|
+
Recorder.add_span(
|
|
36
|
+
category: :custom,
|
|
37
|
+
operation: 'span',
|
|
38
|
+
detail: name.to_s,
|
|
39
|
+
payload: payload,
|
|
40
|
+
duration_ms: duration_ms,
|
|
41
|
+
started_at: start_wall
|
|
42
|
+
)
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def snapshot(data)
|
|
47
|
+
return unless Context.active?
|
|
48
|
+
|
|
49
|
+
payload = data.is_a?(Hash) ? data : { value: data }
|
|
50
|
+
Recorder.add_span(
|
|
51
|
+
category: :snapshot,
|
|
52
|
+
operation: 'snapshot',
|
|
53
|
+
detail: 'snapshot',
|
|
54
|
+
payload: payload,
|
|
55
|
+
duration_ms: 0,
|
|
56
|
+
started_at: Time.now
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def tag(**metadata)
|
|
61
|
+
return unless Context.active?
|
|
62
|
+
|
|
63
|
+
Context.current&.merge_tags!(**metadata)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def build_store
|
|
69
|
+
store = case config.store
|
|
70
|
+
when :sqlite
|
|
71
|
+
Store::Sqlite.new(path: config.sqlite_path)
|
|
72
|
+
when :memory
|
|
73
|
+
Store::Memory.new
|
|
74
|
+
when :postgresql
|
|
75
|
+
conn = config.postgresql_connection || (defined?(ActiveRecord::Base) && ActiveRecord::Base.connection)
|
|
76
|
+
conn ? Store::Postgresql.new(connection: conn) : Store::Sqlite.new(path: config.sqlite_path)
|
|
77
|
+
else
|
|
78
|
+
Store::Sqlite.new(path: config.sqlite_path)
|
|
79
|
+
end
|
|
80
|
+
config.async_store ? Store::Async.new(store) : store
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
require 'dontbugme/version'
|
|
86
|
+
require 'dontbugme/configuration'
|
|
87
|
+
require 'dontbugme/span'
|
|
88
|
+
require 'dontbugme/span_collection'
|
|
89
|
+
require 'dontbugme/trace'
|
|
90
|
+
require 'dontbugme/context'
|
|
91
|
+
require 'dontbugme/source_location'
|
|
92
|
+
require 'dontbugme/recorder'
|
|
93
|
+
require 'dontbugme/subscribers/base'
|
|
94
|
+
require 'dontbugme/subscribers/active_record'
|
|
95
|
+
require 'dontbugme/subscribers/net_http'
|
|
96
|
+
require 'dontbugme/subscribers/redis'
|
|
97
|
+
require 'dontbugme/subscribers/cache'
|
|
98
|
+
require 'dontbugme/subscribers/action_mailer'
|
|
99
|
+
require 'dontbugme/subscribers/active_job'
|
|
100
|
+
require 'dontbugme/store/base'
|
|
101
|
+
require 'dontbugme/store/memory'
|
|
102
|
+
require 'dontbugme/store/sqlite'
|
|
103
|
+
require 'dontbugme/store/postgresql'
|
|
104
|
+
require 'dontbugme/store/async'
|
|
105
|
+
require 'dontbugme/cleanup_job'
|
|
106
|
+
require 'dontbugme/correlation'
|
|
107
|
+
require 'dontbugme/middleware/sidekiq'
|
|
108
|
+
require 'dontbugme/middleware/sidekiq_client'
|
|
109
|
+
require 'dontbugme/middleware/rack'
|
|
110
|
+
require 'dontbugme/formatters/timeline'
|
|
111
|
+
require 'dontbugme/formatters/json'
|
|
112
|
+
require 'dontbugme/formatters/diff'
|
|
113
|
+
require 'dontbugme/cli'
|
|
114
|
+
|
|
115
|
+
# Load Railtie when Rails is present (must be after all other requires)
|
|
116
|
+
if defined?(Rails)
|
|
117
|
+
require 'dontbugme/railtie'
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
|
7
|
+
|
|
8
|
+
def add_route
|
|
9
|
+
route "mount Dontbugme::Engine, at: '/inspector' if Dontbugme.config.enable_web_ui"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create_initializer
|
|
13
|
+
template 'dontbugme.rb', 'config/initializers/dontbugme.rb'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|