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,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :store,
|
|
6
|
+
:sqlite_path,
|
|
7
|
+
:postgresql_connection,
|
|
8
|
+
:enable_web_ui,
|
|
9
|
+
:web_ui_mount_path,
|
|
10
|
+
:recording_mode,
|
|
11
|
+
:record_on_error,
|
|
12
|
+
:record_jobs,
|
|
13
|
+
:record_requests,
|
|
14
|
+
:sample_rate,
|
|
15
|
+
:capture_sql_binds,
|
|
16
|
+
:capture_http_headers,
|
|
17
|
+
:capture_http_body,
|
|
18
|
+
:capture_redis_values,
|
|
19
|
+
:source_mode,
|
|
20
|
+
:source_filter,
|
|
21
|
+
:source_depth,
|
|
22
|
+
:source_stack_limit,
|
|
23
|
+
:max_spans_per_trace,
|
|
24
|
+
:span_overflow_strategy,
|
|
25
|
+
:max_sql_bind_size,
|
|
26
|
+
:max_http_body_size,
|
|
27
|
+
:max_redis_value_size,
|
|
28
|
+
:max_span_detail_size,
|
|
29
|
+
:max_trace_buffer_bytes,
|
|
30
|
+
:retention,
|
|
31
|
+
:max_traces,
|
|
32
|
+
:async_store
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
apply_environment_defaults
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def apply_environment_defaults
|
|
39
|
+
env = defined?(Rails) ? Rails.env.to_s : 'development'
|
|
40
|
+
|
|
41
|
+
case env
|
|
42
|
+
when 'test'
|
|
43
|
+
apply_test_defaults
|
|
44
|
+
when 'production'
|
|
45
|
+
apply_production_defaults
|
|
46
|
+
else
|
|
47
|
+
apply_development_defaults
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def apply_development_defaults
|
|
52
|
+
self.store = :sqlite
|
|
53
|
+
self.sqlite_path = 'tmp/inspector/inspector.db'
|
|
54
|
+
self.postgresql_connection = nil
|
|
55
|
+
self.enable_web_ui = true
|
|
56
|
+
self.web_ui_mount_path = '/inspector'
|
|
57
|
+
self.recording_mode = :always
|
|
58
|
+
self.record_on_error = true
|
|
59
|
+
self.record_jobs = :all
|
|
60
|
+
self.record_requests = :all
|
|
61
|
+
self.sample_rate = 1.0
|
|
62
|
+
self.capture_sql_binds = true
|
|
63
|
+
self.capture_http_headers = []
|
|
64
|
+
self.capture_http_body = false
|
|
65
|
+
self.capture_redis_values = false
|
|
66
|
+
self.source_mode = :full
|
|
67
|
+
self.source_filter = %w[app/ lib/]
|
|
68
|
+
self.source_depth = 3
|
|
69
|
+
self.source_stack_limit = 50
|
|
70
|
+
self.max_spans_per_trace = 1_000
|
|
71
|
+
self.span_overflow_strategy = :count
|
|
72
|
+
self.max_sql_bind_size = 4_096
|
|
73
|
+
self.max_http_body_size = 8_192
|
|
74
|
+
self.max_redis_value_size = 512
|
|
75
|
+
self.max_span_detail_size = 8_192
|
|
76
|
+
self.max_trace_buffer_bytes = 10 * 1024 * 1024 # 10 MB
|
|
77
|
+
self.retention = retention_seconds(72, :hours)
|
|
78
|
+
self.max_traces = 10_000
|
|
79
|
+
self.async_store = false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply_test_defaults
|
|
83
|
+
apply_development_defaults
|
|
84
|
+
self.store = :memory
|
|
85
|
+
self.recording_mode = :off
|
|
86
|
+
self.enable_web_ui = false
|
|
87
|
+
self.record_on_error = false
|
|
88
|
+
self.max_trace_buffer_bytes = 5 * 1024 * 1024 # 5 MB
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply_production_defaults
|
|
92
|
+
apply_development_defaults
|
|
93
|
+
self.store = :postgresql
|
|
94
|
+
self.enable_web_ui = false
|
|
95
|
+
self.recording_mode = :selective
|
|
96
|
+
self.record_on_error = true
|
|
97
|
+
self.capture_sql_binds = false
|
|
98
|
+
self.source_mode = :shallow
|
|
99
|
+
self.source_depth = 1
|
|
100
|
+
self.source_stack_limit = 30
|
|
101
|
+
self.max_spans_per_trace = 500
|
|
102
|
+
self.max_sql_bind_size = 512
|
|
103
|
+
self.max_http_body_size = 1_024
|
|
104
|
+
self.max_span_detail_size = 2_048
|
|
105
|
+
self.max_trace_buffer_bytes = 5 * 1024 * 1024 # 5 MB
|
|
106
|
+
self.retention = retention_seconds(24, :hours)
|
|
107
|
+
self.async_store = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def recording?
|
|
111
|
+
return false if recording_mode == :off
|
|
112
|
+
return true if recording_mode == :always
|
|
113
|
+
|
|
114
|
+
# :selective — caller (Recorder) decides based on record_jobs, record_requests, etc.
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def should_record_job?(job_class)
|
|
119
|
+
return false if record_jobs == :none
|
|
120
|
+
return true if record_jobs == :all
|
|
121
|
+
return record_jobs.include?(job_class.to_s) if record_jobs.is_a?(Array)
|
|
122
|
+
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def should_record_request?(env)
|
|
127
|
+
return false if record_requests == :none
|
|
128
|
+
return true if record_requests == :all
|
|
129
|
+
return record_requests.call(env) if record_requests.respond_to?(:call)
|
|
130
|
+
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def retention_seconds(value, unit)
|
|
137
|
+
return value if value.is_a?(Integer)
|
|
138
|
+
return value.to_i if defined?(ActiveSupport) && value.respond_to?(:to_i)
|
|
139
|
+
|
|
140
|
+
case unit
|
|
141
|
+
when :hours then value * 3600
|
|
142
|
+
when :days then value * 86400
|
|
143
|
+
else value
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
class Context
|
|
5
|
+
KEY = :dontbugme_trace
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def current
|
|
9
|
+
Thread.current[KEY]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def current=(trace)
|
|
13
|
+
Thread.current[KEY] = trace
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def active?
|
|
17
|
+
!current.nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear!
|
|
21
|
+
Thread.current[KEY] = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Correlation
|
|
5
|
+
KEY = :dontbugme_correlation_id
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def current
|
|
9
|
+
Thread.current[KEY]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def current=(id)
|
|
13
|
+
Thread.current[KEY] = id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate
|
|
17
|
+
"corr_#{SecureRandom.hex(8)}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clear!
|
|
21
|
+
Thread.current[KEY] = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Dontbugme
|
|
6
|
+
|
|
7
|
+
# Mount in config/routes.rb:
|
|
8
|
+
# mount Dontbugme::Engine, at: '/inspector'
|
|
9
|
+
# Enable/disable via config.enable_web_ui (default: true in dev, false in prod)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
|
|
5
|
+
module Dontbugme
|
|
6
|
+
module Formatters
|
|
7
|
+
class Diff
|
|
8
|
+
def self.format(trace_a, trace_b)
|
|
9
|
+
new(trace_a, trace_b).format
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(trace_a, trace_b)
|
|
13
|
+
@trace_a = trace_a
|
|
14
|
+
@trace_b = trace_b
|
|
15
|
+
@alignment = align_spans
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format
|
|
19
|
+
lines = []
|
|
20
|
+
lines << ''
|
|
21
|
+
lines << " #{@trace_a.identifier} — Execution Diff"
|
|
22
|
+
lines << ' ' + ('─' * 60)
|
|
23
|
+
lines << ''
|
|
24
|
+
lines << " Trace A: #{@trace_a.id} (#{@trace_a.status}, #{duration_str(@trace_a)})"
|
|
25
|
+
lines << " Trace B: #{@trace_b.id} (#{@trace_b.status}, #{duration_str(@trace_b)})"
|
|
26
|
+
lines << ''
|
|
27
|
+
|
|
28
|
+
@alignment.each do |result|
|
|
29
|
+
lines << format_result(result)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
lines << ''
|
|
33
|
+
lines.join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def align_spans
|
|
39
|
+
spans_a = spans_with_error(@trace_a)
|
|
40
|
+
spans_b = spans_with_error(@trace_b)
|
|
41
|
+
results = []
|
|
42
|
+
i = j = 0
|
|
43
|
+
|
|
44
|
+
while i < spans_a.size || j < spans_b.size
|
|
45
|
+
span_a = spans_a[i]
|
|
46
|
+
span_b = spans_b[j]
|
|
47
|
+
|
|
48
|
+
if span_a.nil?
|
|
49
|
+
results << { type: :new, span_a: nil, span_b: span_b }
|
|
50
|
+
j += 1
|
|
51
|
+
elsif span_b.nil?
|
|
52
|
+
results << { type: :missing, span_a: span_a, span_b: nil }
|
|
53
|
+
i += 1
|
|
54
|
+
else
|
|
55
|
+
key_a = span_key(span_a)
|
|
56
|
+
key_b = span_key(span_b)
|
|
57
|
+
|
|
58
|
+
if key_a == key_b
|
|
59
|
+
if span_equal?(span_a, span_b)
|
|
60
|
+
results << { type: :identical, span_a: span_a, span_b: span_b }
|
|
61
|
+
else
|
|
62
|
+
results << { type: :changed, span_a: span_a, span_b: span_b }
|
|
63
|
+
end
|
|
64
|
+
i += 1
|
|
65
|
+
j += 1
|
|
66
|
+
elsif key_a < key_b
|
|
67
|
+
results << { type: :missing, span_a: span_a, span_b: nil }
|
|
68
|
+
i += 1
|
|
69
|
+
else
|
|
70
|
+
results << { type: :new, span_a: nil, span_b: span_b }
|
|
71
|
+
j += 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
results
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def spans_with_error(trace)
|
|
80
|
+
spans = trace.raw_spans.dup
|
|
81
|
+
if trace.error
|
|
82
|
+
error_span = OpenStruct.new(
|
|
83
|
+
category: :custom,
|
|
84
|
+
operation: 'EXCEPTION',
|
|
85
|
+
detail: "#{trace.error[:class]}: #{trace.error[:message]}",
|
|
86
|
+
payload: trace.error
|
|
87
|
+
)
|
|
88
|
+
spans << error_span
|
|
89
|
+
end
|
|
90
|
+
spans
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def span_key(span)
|
|
94
|
+
detail = normalize_detail(span.detail)
|
|
95
|
+
"#{span.category}:#{span.operation}:#{detail}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize_detail(detail)
|
|
99
|
+
return '' if detail.nil?
|
|
100
|
+
# Normalize SQL: collapse whitespace, remove exact values for comparison
|
|
101
|
+
detail = detail.to_s.strip.gsub(/\s+/, ' ')
|
|
102
|
+
# Truncate for alignment (compare structure, not values)
|
|
103
|
+
detail[0, 80]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def span_equal?(span_a, span_b)
|
|
107
|
+
return false unless span_a.category == span_b.category
|
|
108
|
+
return false unless span_a.operation == span_b.operation
|
|
109
|
+
|
|
110
|
+
case span_a.category
|
|
111
|
+
when :http
|
|
112
|
+
(span_a.payload[:status] || span_a.payload['status']) == (span_b.payload[:status] || span_b.payload['status'])
|
|
113
|
+
when :sql
|
|
114
|
+
normalize_detail(span_a.detail) == normalize_detail(span_b.detail)
|
|
115
|
+
else
|
|
116
|
+
normalize_detail(span_a.detail) == normalize_detail(span_b.detail)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def format_result(result)
|
|
121
|
+
case result[:type]
|
|
122
|
+
when :identical
|
|
123
|
+
" IDENTICAL #{format_span_short(result[:span_a])}"
|
|
124
|
+
when :changed
|
|
125
|
+
lines = [" CHANGED #{format_span_short(result[:span_a])}"]
|
|
126
|
+
lines << " A: #{format_span_detail(result[:span_a])}"
|
|
127
|
+
lines << " B: #{format_span_detail(result[:span_b])} ←"
|
|
128
|
+
lines.join("\n")
|
|
129
|
+
when :missing
|
|
130
|
+
" MISSING #{format_span_short(result[:span_a])} ← never reached in B"
|
|
131
|
+
when :new
|
|
132
|
+
span_b = result[:span_b]
|
|
133
|
+
if span_b.operation == 'EXCEPTION'
|
|
134
|
+
err = span_b.payload
|
|
135
|
+
detail = err ? "#{err[:class]}: #{truncate(err[:message].to_s, 40)}" : 'EXCEPTION'
|
|
136
|
+
" NEW EXCEPTION #{detail} ← only in B"
|
|
137
|
+
else
|
|
138
|
+
" NEW #{format_span_short(span_b)} ← only in B"
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
''
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_span_short(span)
|
|
146
|
+
return '' unless span
|
|
147
|
+
|
|
148
|
+
detail = truncate(span.detail, 50)
|
|
149
|
+
"#{span.category.to_s.upcase} #{span.operation} #{detail}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def format_span_detail(span)
|
|
153
|
+
case span.category
|
|
154
|
+
when :http
|
|
155
|
+
status = span.payload[:status] || span.payload['status']
|
|
156
|
+
duration = span.duration_ms ? "#{span.duration_ms.round}ms" : ''
|
|
157
|
+
"#{status} #{status_label(status)} (#{duration})".strip
|
|
158
|
+
when :sql
|
|
159
|
+
truncate(span.detail, 60)
|
|
160
|
+
else
|
|
161
|
+
truncate(span.detail, 60)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def status_label(code)
|
|
166
|
+
return '' unless code
|
|
167
|
+
return 'OK' if code.to_i.between?(200, 299)
|
|
168
|
+
return 'Timeout' if code.to_i == 408 || code.to_i == 504
|
|
169
|
+
return 'Error' if code.to_i >= 400
|
|
170
|
+
|
|
171
|
+
''
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def duration_str(trace)
|
|
175
|
+
ms = trace.duration_ms
|
|
176
|
+
ms ? "#{ms.round}ms" : 'N/A'
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def truncate(str, max)
|
|
180
|
+
return '' if str.nil? || str.empty?
|
|
181
|
+
return str if str.length <= max
|
|
182
|
+
|
|
183
|
+
"#{str[0, max - 3]}..."
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Formatters
|
|
5
|
+
class Timeline
|
|
6
|
+
def self.format(trace, only: nil, slow: nil)
|
|
7
|
+
new(trace, only: only, slow: slow).format
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(trace, only: nil, slow: nil)
|
|
11
|
+
@trace = trace
|
|
12
|
+
@only = only&.to_sym
|
|
13
|
+
@slow = slow
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format
|
|
17
|
+
lines = []
|
|
18
|
+
lines << ''
|
|
19
|
+
lines << " #{@trace.identifier}"
|
|
20
|
+
lines << " Status: #{@trace.status} | Duration: #{duration_str} | Recorded: #{time_ago}"
|
|
21
|
+
lines << ''
|
|
22
|
+
lines << ' TIMELINE'
|
|
23
|
+
lines << ' ' + ('─' * 60)
|
|
24
|
+
lines << ''
|
|
25
|
+
lines << format_start_span
|
|
26
|
+
lines << ''
|
|
27
|
+
|
|
28
|
+
spans_to_show.each do |span|
|
|
29
|
+
lines << format_span(span)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
lines << ''
|
|
33
|
+
lines << format_finish_span
|
|
34
|
+
|
|
35
|
+
if @trace.truncated_spans_count.to_i.positive?
|
|
36
|
+
lines << ''
|
|
37
|
+
lines << " ... #{@trace.truncated_spans_count} additional spans truncated"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
lines << ''
|
|
41
|
+
lines.join("\n")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def format_start_span
|
|
47
|
+
label = case @trace.kind
|
|
48
|
+
when :sidekiq then 'Job started'
|
|
49
|
+
when :request then 'Request started'
|
|
50
|
+
else 'Started'
|
|
51
|
+
end
|
|
52
|
+
" 0.0ms ▸ #{label}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def format_finish_span
|
|
56
|
+
label = case @trace.kind
|
|
57
|
+
when :sidekiq then 'Job finished'
|
|
58
|
+
when :request then 'Request finished'
|
|
59
|
+
else 'Finished'
|
|
60
|
+
end
|
|
61
|
+
duration = @trace.duration_ms ? "#{@trace.duration_ms.round(1)}ms" : '0ms'
|
|
62
|
+
" #{duration} ▸ #{label}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def spans_to_show
|
|
66
|
+
spans = @trace.raw_spans
|
|
67
|
+
spans = spans.select { |s| s.category == @only } if @only
|
|
68
|
+
spans = spans.select { |s| s.duration_ms.to_f >= @slow } if @slow
|
|
69
|
+
spans
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def format_span(span)
|
|
73
|
+
offset = span.started_at.to_f.round(1)
|
|
74
|
+
duration = span.duration_ms ? "(#{span.duration_ms.round(1)}ms)" : ''
|
|
75
|
+
detail = format_span_detail(span)
|
|
76
|
+
line = " #{offset}ms ▸ #{span.category.to_s.upcase} #{span.operation} #{detail} #{duration}".strip
|
|
77
|
+
lines = [line]
|
|
78
|
+
lines << " → #{span.source}" if span.source && !span.source.empty?
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def format_span_detail(span)
|
|
83
|
+
case span.category
|
|
84
|
+
when :http
|
|
85
|
+
status = span.payload[:status] || span.payload['status']
|
|
86
|
+
status_str = status ? " → #{status}" : ''
|
|
87
|
+
truncate_detail(span.detail + status_str, 55)
|
|
88
|
+
else
|
|
89
|
+
truncate_detail(span.detail, 50)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def truncate_detail(str, max_len)
|
|
94
|
+
return '' if str.nil? || str.empty?
|
|
95
|
+
|
|
96
|
+
str = str.strip
|
|
97
|
+
return str if str.length <= max_len
|
|
98
|
+
|
|
99
|
+
"#{str[0, max_len - 3]}..."
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def duration_str
|
|
103
|
+
ms = @trace.duration_ms
|
|
104
|
+
ms ? "#{ms.round}ms" : 'N/A'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def time_ago
|
|
108
|
+
return 'N/A' unless @trace.started_at_utc
|
|
109
|
+
|
|
110
|
+
sec = Time.now - @trace.started_at_utc
|
|
111
|
+
if sec < 60 then "#{sec.round} sec ago"
|
|
112
|
+
elsif sec < 3600 then "#{(sec / 60).round} min ago"
|
|
113
|
+
elsif sec < 86400 then "#{(sec / 3600).round} hours ago"
|
|
114
|
+
else "#{(sec / 86400).round} days ago"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Middleware
|
|
5
|
+
class Rack
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
return @app.call(env) unless Dontbugme.config.recording?
|
|
12
|
+
|
|
13
|
+
request = ::Rack::Request.new(env)
|
|
14
|
+
request_id = env['action_dispatch.request_id'] || request.get_header('HTTP_X_REQUEST_ID') || SecureRandom.uuid
|
|
15
|
+
correlation_id = env['HTTP_X_CORRELATION_ID'] || Correlation.generate
|
|
16
|
+
Correlation.current = correlation_id
|
|
17
|
+
|
|
18
|
+
method = request.request_method
|
|
19
|
+
path = request.path
|
|
20
|
+
identifier = "#{method} #{path}"
|
|
21
|
+
|
|
22
|
+
metadata = {
|
|
23
|
+
request_id: request_id,
|
|
24
|
+
correlation_id: correlation_id,
|
|
25
|
+
method: method,
|
|
26
|
+
path: path
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Recorder.record(kind: :request, identifier: identifier, metadata: metadata) do
|
|
30
|
+
@app.call(env)
|
|
31
|
+
end
|
|
32
|
+
ensure
|
|
33
|
+
Correlation.clear!
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Middleware
|
|
5
|
+
class Sidekiq
|
|
6
|
+
def call(_worker, job, _queue)
|
|
7
|
+
return yield unless Dontbugme.config.recording?
|
|
8
|
+
|
|
9
|
+
job_class = job['class'] || job[:class] || 'Unknown'
|
|
10
|
+
jid = job['jid'] || job[:jid] || SecureRandom.hex(8)
|
|
11
|
+
correlation_id = job['correlation_id'] || job[:correlation_id] || Correlation.current
|
|
12
|
+
Correlation.current = correlation_id
|
|
13
|
+
|
|
14
|
+
identifier = "#{job_class} (jid=#{jid})"
|
|
15
|
+
|
|
16
|
+
metadata = {
|
|
17
|
+
jid: jid,
|
|
18
|
+
queue: job['queue'] || job[:queue],
|
|
19
|
+
args: job['args'] || job[:args],
|
|
20
|
+
correlation_id: correlation_id
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Recorder.record(kind: :sidekiq, identifier: identifier, metadata: metadata) do
|
|
24
|
+
yield
|
|
25
|
+
end
|
|
26
|
+
ensure
|
|
27
|
+
Correlation.clear!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dontbugme
|
|
4
|
+
module Middleware
|
|
5
|
+
class SidekiqClient
|
|
6
|
+
def call(_worker_class, job, _queue, _redis_pool)
|
|
7
|
+
if Correlation.current
|
|
8
|
+
job['correlation_id'] = Correlation.current
|
|
9
|
+
end
|
|
10
|
+
yield
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dontbugme'
|
|
4
|
+
require 'dontbugme/engine'
|
|
5
|
+
|
|
6
|
+
module Dontbugme
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
config.dontbugme = ActiveSupport::OrderedOptions.new
|
|
9
|
+
|
|
10
|
+
initializer 'dontbugme.configure' do |app|
|
|
11
|
+
# Configuration defaults are applied in Configuration#initialize
|
|
12
|
+
# Merge any app-level config (e.g. config.dontbugme.store = :sqlite)
|
|
13
|
+
config = Dontbugme.config
|
|
14
|
+
app.config.dontbugme.each { |k, v| config.send("#{k}=", v) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer 'dontbugme.subscribers' do
|
|
18
|
+
Dontbugme::Subscribers::ActiveRecord.subscribe
|
|
19
|
+
Dontbugme::Subscribers::NetHttp.subscribe
|
|
20
|
+
Dontbugme::Subscribers::Redis.subscribe
|
|
21
|
+
Dontbugme::Subscribers::Cache.subscribe
|
|
22
|
+
Dontbugme::Subscribers::ActionMailer.subscribe
|
|
23
|
+
Dontbugme::Subscribers::ActiveJob.subscribe
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
config.after_initialize do
|
|
27
|
+
# Sidekiq middleware
|
|
28
|
+
if defined?(Sidekiq)
|
|
29
|
+
Sidekiq.configure_server do |config|
|
|
30
|
+
config.server_middleware do |chain|
|
|
31
|
+
chain.add Dontbugme::Middleware::Sidekiq
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
Sidekiq.configure_client do |config|
|
|
35
|
+
config.client_middleware do |chain|
|
|
36
|
+
chain.add Dontbugme::Middleware::SidekiqClient
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rack middleware
|
|
42
|
+
if defined?(Rails::Application)
|
|
43
|
+
Rails.application.config.middleware.insert 0, Dontbugme::Middleware::Rack
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|