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.
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Koderift
4
4
  module Rails
5
- VERSION = '1.3.4'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  end
@@ -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.3.4
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: