koderift-rails 1.3.4 → 2.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 +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +183 -3
- data/lib/koderift/rails/breadcrumb.rb +70 -0
- data/lib/koderift/rails/capture.rb +514 -0
- data/lib/koderift/rails/configuration.rb +38 -0
- data/lib/koderift/rails/controller.rb +21 -0
- data/lib/koderift/rails/event.rb +27 -0
- data/lib/koderift/rails/instrumentation.rb +64 -19
- data/lib/koderift/rails/lograge_config.rb +7 -2
- data/lib/koderift/rails/railtie.rb +14 -0
- data/lib/koderift/rails/rake_integration.rb +43 -0
- data/lib/koderift/rails/rum_helper.rb +60 -4
- data/lib/koderift/rails/scope.rb +163 -0
- data/lib/koderift/rails/sidekiq_middleware.rb +24 -1
- data/lib/koderift/rails/tracing.rb +195 -0
- data/lib/koderift/rails/version.rb +1 -1
- data/lib/koderift/rails.rb +187 -0
- metadata +7 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'koderift/rails/instrumentation'
|
|
6
|
+
require 'koderift/rails/capture'
|
|
7
|
+
|
|
8
|
+
module Koderift
|
|
9
|
+
module Rails
|
|
10
|
+
# Sentry-compatible custom instrumentation, backed by the span pipeline the
|
|
11
|
+
# gem already has. A child span becomes a `custom` span in log_spans and
|
|
12
|
+
# renders in the APM trace next to the SQL, partial and HTTP spans.
|
|
13
|
+
module Tracing
|
|
14
|
+
TRANSACTION_KEY = :koderift_transaction
|
|
15
|
+
MAX_SPAN_NAME = 2_000
|
|
16
|
+
|
|
17
|
+
# Handed to a with_child_span block. set_data and set_description are
|
|
18
|
+
# real: they shape the span's detail and name in the emitted trace.
|
|
19
|
+
class ChildSpan
|
|
20
|
+
attr_reader :op, :description
|
|
21
|
+
|
|
22
|
+
def initialize(op: nil, description: nil)
|
|
23
|
+
@op = op
|
|
24
|
+
@description = description
|
|
25
|
+
@data = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set_data(key, value)
|
|
29
|
+
@data[key.to_s] = value
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def set_description(value)
|
|
34
|
+
@description = value
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def name
|
|
39
|
+
[@description, @op].map(&:to_s).reject(&:empty?).first || 'custom'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def detail
|
|
43
|
+
payload = @data.dup
|
|
44
|
+
payload['op'] = @op.to_s unless @op.to_s.empty?
|
|
45
|
+
payload.empty? ? nil : JSON.generate(payload)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A unit of work that is neither an HTTP request nor a Sidekiq job — the
|
|
52
|
+
# nightly rake task case. It runs through Instrumentation's subscriptions
|
|
53
|
+
# exactly as those two do, so its SQL, HTTP and custom spans reach the
|
|
54
|
+
# APM trace view.
|
|
55
|
+
class Transaction
|
|
56
|
+
attr_reader :name, :op, :request_id, :trace_id
|
|
57
|
+
|
|
58
|
+
def initialize(name:, op: nil)
|
|
59
|
+
@name = name.to_s
|
|
60
|
+
@op = op
|
|
61
|
+
@finished = false
|
|
62
|
+
|
|
63
|
+
# A transaction MUST carry a request_id.
|
|
64
|
+
# IngestLogBatchJob#build_span_rows does `next if request_id.blank?`,
|
|
65
|
+
# so without one every span collected here is dropped before it
|
|
66
|
+
# reaches log_spans — the trace would arrive empty, with no error
|
|
67
|
+
# anywhere to explain why.
|
|
68
|
+
@request_id = SecureRandom.uuid
|
|
69
|
+
@trace_id = Thread.current[:koderift_trace_id] || SecureRandom.uuid
|
|
70
|
+
|
|
71
|
+
@previous_trace_id = Thread.current[:koderift_trace_id]
|
|
72
|
+
@previous_context = Thread.current[Capture::CONTEXT_KEY]
|
|
73
|
+
|
|
74
|
+
Thread.current[:koderift_trace_id] = @trace_id
|
|
75
|
+
Thread.current[Capture::CONTEXT_KEY] = {
|
|
76
|
+
component: component,
|
|
77
|
+
action: action,
|
|
78
|
+
request_id: @request_id
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
82
|
+
@state = Instrumentation.start_capture
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def finished?
|
|
86
|
+
@finished
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def finish
|
|
90
|
+
return nil if @finished
|
|
91
|
+
|
|
92
|
+
@finished = true
|
|
93
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at) * 1000).round
|
|
94
|
+
|
|
95
|
+
Instrumentation.stop_capture(@state)
|
|
96
|
+
result = Instrumentation.build_result(@state)
|
|
97
|
+
|
|
98
|
+
Capture.transaction(
|
|
99
|
+
component: component,
|
|
100
|
+
action: action,
|
|
101
|
+
message: "Completed in #{(duration_ms / 1000.0).round(1)}s",
|
|
102
|
+
duration_ms: duration_ms,
|
|
103
|
+
request_id: @request_id,
|
|
104
|
+
trace_id: @trace_id,
|
|
105
|
+
result: result
|
|
106
|
+
)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
nil
|
|
109
|
+
ensure
|
|
110
|
+
Thread.current[:koderift_trace_id] = @previous_trace_id
|
|
111
|
+
Thread.current[Capture::CONTEXT_KEY] = @previous_context
|
|
112
|
+
Tracing.clear_current(self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def component
|
|
116
|
+
part = @name.split('#').first.to_s
|
|
117
|
+
part.empty? ? 'transaction' : part
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def action
|
|
121
|
+
part = @name.split('#')[1].to_s
|
|
122
|
+
return part unless part.empty?
|
|
123
|
+
|
|
124
|
+
op = @op.to_s
|
|
125
|
+
op.empty? ? 'call' : op
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class << self
|
|
130
|
+
def with_child_span(op: nil, description: nil)
|
|
131
|
+
span = ChildSpan.new(op: op, description: description)
|
|
132
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
133
|
+
begin
|
|
134
|
+
yield span
|
|
135
|
+
ensure
|
|
136
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
137
|
+
record(span, elapsed)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns the Transaction when called without a block, so
|
|
142
|
+
# `txn = Koderift.start_transaction(name: '...')` / `txn.finish` works
|
|
143
|
+
# as it does in Sentry. PREFER the block form: it finishes in an ensure,
|
|
144
|
+
# and an abandoned transaction leaves four ActiveSupport::Notifications
|
|
145
|
+
# subscribers attached to the process. Starting a new transaction
|
|
146
|
+
# finishes an abandoned one on the same thread, which bounds that leak
|
|
147
|
+
# to one, but does not make it free.
|
|
148
|
+
def start_transaction(name: nil, op: nil)
|
|
149
|
+
previous = Thread.current[TRANSACTION_KEY]
|
|
150
|
+
previous.finish if previous.is_a?(Transaction) && !previous.finished?
|
|
151
|
+
|
|
152
|
+
transaction = Transaction.new(name: name || op.to_s, op: op)
|
|
153
|
+
Thread.current[TRANSACTION_KEY] = transaction
|
|
154
|
+
return transaction unless block_given?
|
|
155
|
+
|
|
156
|
+
begin
|
|
157
|
+
yield transaction
|
|
158
|
+
ensure
|
|
159
|
+
transaction.finish
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def current_transaction
|
|
164
|
+
Thread.current[TRANSACTION_KEY]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def clear_current(transaction)
|
|
168
|
+
return unless Thread.current[TRANSACTION_KEY].equal?(transaction)
|
|
169
|
+
|
|
170
|
+
Thread.current[TRANSACTION_KEY] = nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Outside a captured unit of work there is no array to append to, so the
|
|
176
|
+
# span is simply not recorded. That is inertness for want of a place to
|
|
177
|
+
# put it, not a stub: inside a request, job or transaction the very same
|
|
178
|
+
# call produces a real span.
|
|
179
|
+
def record(span, duration_ms)
|
|
180
|
+
spans = Thread.current[:koderift_custom_spans]
|
|
181
|
+
return nil unless spans.is_a?(Array)
|
|
182
|
+
|
|
183
|
+
name = span.name.to_s
|
|
184
|
+
spans << {
|
|
185
|
+
name: name.length > MAX_SPAN_NAME ? name[0, MAX_SPAN_NAME] : name,
|
|
186
|
+
duration_ms: duration_ms,
|
|
187
|
+
detail: span.detail
|
|
188
|
+
}
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
data/lib/koderift/rails.rb
CHANGED
|
@@ -4,6 +4,11 @@ require 'koderift/rails/version'
|
|
|
4
4
|
require 'koderift/rails/configuration'
|
|
5
5
|
require 'koderift/rails/instrumentation'
|
|
6
6
|
require 'koderift/rails/lograge_config'
|
|
7
|
+
require 'koderift/rails/event'
|
|
8
|
+
require 'koderift/rails/breadcrumb'
|
|
9
|
+
require 'koderift/rails/scope'
|
|
10
|
+
require 'koderift/rails/capture'
|
|
11
|
+
require 'koderift/rails/tracing'
|
|
7
12
|
require 'koderift/rails/controller'
|
|
8
13
|
require 'koderift/rails/rum_helper'
|
|
9
14
|
require 'koderift/rails/railtie' if defined?(::Rails::Railtie)
|
|
@@ -21,7 +26,189 @@ module Koderift
|
|
|
21
26
|
|
|
22
27
|
def reset!
|
|
23
28
|
@configuration = Configuration.new
|
|
29
|
+
Capture.reset_rate_limiter!
|
|
30
|
+
Scope.reset!
|
|
31
|
+
Breadcrumbs.clear
|
|
32
|
+
RumHelper.reset_warnings!
|
|
24
33
|
end
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------- capture
|
|
36
|
+
|
|
37
|
+
def capture_exception(error, **options, &block)
|
|
38
|
+
Capture.exception(error, **options, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def capture_message(text, **options, &block)
|
|
42
|
+
Capture.message(text, **options, &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def capture_event(event)
|
|
46
|
+
Capture.event(event)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def last_event_id
|
|
50
|
+
Capture.last_event_id
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialized?
|
|
54
|
+
Capture.initialized?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def exception_captured?(error)
|
|
58
|
+
Capture.exception_captured?(error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------ scope
|
|
62
|
+
|
|
63
|
+
def get_current_scope
|
|
64
|
+
Scope.current
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def configure_scope
|
|
68
|
+
yield Scope.current if block_given?
|
|
69
|
+
Scope.current
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def with_scope
|
|
73
|
+
Scope.push
|
|
74
|
+
begin
|
|
75
|
+
yield Scope.current
|
|
76
|
+
ensure
|
|
77
|
+
Scope.pop
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set_user(user)
|
|
82
|
+
Scope.current.set_user(user)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_tags(tags)
|
|
86
|
+
Scope.current.set_tags(tags)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def set_tag(key, value)
|
|
90
|
+
Scope.current.set_tag(key, value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def set_extras(extras)
|
|
94
|
+
Scope.current.set_extras(extras)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def set_extra(key, value)
|
|
98
|
+
Scope.current.set_extra(key, value)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set_context(key, value)
|
|
102
|
+
Scope.current.set_context(key, value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_breadcrumb(crumb)
|
|
106
|
+
Breadcrumbs.add(crumb)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------- tracing
|
|
110
|
+
|
|
111
|
+
def start_transaction(name: nil, op: nil, &block)
|
|
112
|
+
Tracing.start_transaction(name: name, op: op, &block)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def with_child_span(op: nil, description: nil, &block)
|
|
116
|
+
Tracing.with_child_span(op: op, description: description, &block)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Sentry-shaped constant, so `Sentry::Breadcrumb.new(...)` becomes
|
|
122
|
+
# `Koderift::Breadcrumb.new(...)` under the same find-and-replace that
|
|
123
|
+
# rewrites the method calls.
|
|
124
|
+
Breadcrumb = Rails::Breadcrumb
|
|
125
|
+
|
|
126
|
+
# The migration surface. An app instrumented with Sentry swaps the namespace
|
|
127
|
+
# and keeps its call sites, including the idiom this API was built for:
|
|
128
|
+
#
|
|
129
|
+
# event_id = Koderift.capture_exception(error)&.event_id
|
|
130
|
+
#
|
|
131
|
+
# Delegators are written out rather than metaprogrammed so the signatures are
|
|
132
|
+
# greppable and `method(:capture_exception).parameters` tells the truth.
|
|
133
|
+
class << self
|
|
134
|
+
def capture_exception(error, **options, &block)
|
|
135
|
+
Rails.capture_exception(error, **options, &block)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def capture_message(text, **options, &block)
|
|
139
|
+
Rails.capture_message(text, **options, &block)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def capture_event(event)
|
|
143
|
+
Rails.capture_event(event)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def last_event_id
|
|
147
|
+
Rails.last_event_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def initialized?
|
|
151
|
+
Rails.initialized?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def exception_captured?(error)
|
|
155
|
+
Rails.exception_captured?(error)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def get_current_scope
|
|
159
|
+
Rails.get_current_scope
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def configure_scope(&block)
|
|
163
|
+
Rails.configure_scope(&block)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def with_scope(&block)
|
|
167
|
+
Rails.with_scope(&block)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def set_user(user)
|
|
171
|
+
Rails.set_user(user)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def set_tags(tags)
|
|
175
|
+
Rails.set_tags(tags)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def set_tag(key, value)
|
|
179
|
+
Rails.set_tag(key, value)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def set_extras(extras)
|
|
183
|
+
Rails.set_extras(extras)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def set_extra(key, value)
|
|
187
|
+
Rails.set_extra(key, value)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def set_context(key, value)
|
|
191
|
+
Rails.set_context(key, value)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def add_breadcrumb(crumb)
|
|
195
|
+
Rails.add_breadcrumb(crumb)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def start_transaction(name: nil, op: nil, &block)
|
|
199
|
+
Rails.start_transaction(name: name, op: op, &block)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def with_child_span(op: nil, description: nil, &block)
|
|
203
|
+
Rails.with_child_span(op: op, description: description, &block)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def configure(&block)
|
|
207
|
+
Rails.configure(&block)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def configuration
|
|
211
|
+
Rails.configuration
|
|
25
212
|
end
|
|
26
213
|
end
|
|
27
214
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: koderift-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koderift
|
|
@@ -110,13 +110,19 @@ files:
|
|
|
110
110
|
- README.md
|
|
111
111
|
- lib/koderift-rails.rb
|
|
112
112
|
- lib/koderift/rails.rb
|
|
113
|
+
- lib/koderift/rails/breadcrumb.rb
|
|
114
|
+
- lib/koderift/rails/capture.rb
|
|
113
115
|
- lib/koderift/rails/configuration.rb
|
|
114
116
|
- lib/koderift/rails/controller.rb
|
|
117
|
+
- lib/koderift/rails/event.rb
|
|
115
118
|
- lib/koderift/rails/instrumentation.rb
|
|
116
119
|
- lib/koderift/rails/lograge_config.rb
|
|
117
120
|
- lib/koderift/rails/railtie.rb
|
|
121
|
+
- lib/koderift/rails/rake_integration.rb
|
|
118
122
|
- lib/koderift/rails/rum_helper.rb
|
|
123
|
+
- lib/koderift/rails/scope.rb
|
|
119
124
|
- lib/koderift/rails/sidekiq_middleware.rb
|
|
125
|
+
- lib/koderift/rails/tracing.rb
|
|
120
126
|
- lib/koderift/rails/version.rb
|
|
121
127
|
homepage: https://github.com/koderift/koderift-rails
|
|
122
128
|
licenses:
|