opentrace 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbe3de5afdb5f92afcef49d368b8f8b3714ca17fe453b168066f53a5b0e5e1ba
4
- data.tar.gz: 8f749e943951c939e7daa8f93a454bb0c89646bc295c0550c680064ebf38db16
3
+ metadata.gz: fd043296872a0f5241acc4f8331bc2d6c6f7e39628e0501fc6e0480af7c423f9
4
+ data.tar.gz: b37915c9ed52898d6607a668c264c573f8d2012d937ec4053712ff2c65fb741e
5
5
  SHA512:
6
- metadata.gz: e3ecb7c4b649951f64e1090bb7ea06da69cb603dcd44534c9a5e71d76eb697239fddf2267cd2a5e0cff2aa1bf86dc78be6733be754df8e5c6a435967d90809a8
7
- data.tar.gz: 3625d9de1cd2dda011f69283b34a17964450cda3c44c0f19431fb450e2a24328f1c260a12ea9929331a54866ac90afd07addd65e16cb2eee9e42a939807f3a69
6
+ metadata.gz: a9567eefab80204781b112f43f915a755ad55327eef2255b7b9c655da9eb36e4eed55c6617e1711d00bbd74ec9dd1f88332b0f0c0c4e2e0562e75a1fdcb8a9fe
7
+ data.tar.gz: 11bf43482a27798a4bcf20c7cb700a565692d2b863d3011feb403df97e039b2b75cd35e0e53f12d7b17a107b7d07ef1959c0539d9507397376bb8901afcb9346
@@ -11,7 +11,6 @@ module OpenTrace
11
11
  class Client
12
12
  MAX_QUEUE_SIZE = 1000
13
13
  PAYLOAD_MAX_BYTES = 262_144 # 256 KB (default; use config.max_payload_bytes to override)
14
- POLL_INTERVAL = 0.05 # 50ms
15
14
  MAX_RATE_LIMIT_BACKOFF = 60 # Cap Retry-After at 60 seconds
16
15
  API_VERSION = 1
17
16
 
@@ -95,12 +94,13 @@ module OpenTrace
95
94
  end
96
95
 
97
96
  def reset_after_fork!
98
- # After fork, the old thread/queue/mutex from the parent are dead.
97
+ # After fork, the old thread/queue/mutex/connection from the parent are dead.
99
98
  # Re-create everything cleanly in the child process.
100
99
  @pid = Process.pid
101
100
  @queue = Thread::Queue.new
102
101
  @mutex = Mutex.new
103
102
  @thread = nil
103
+ @http = nil # Parent's connection is unusable after fork
104
104
  @circuit_breaker.reset!
105
105
  @rate_limit_until = nil
106
106
  @auth_suspended = false
@@ -143,6 +143,8 @@ module OpenTrace
143
143
  end
144
144
  rescue Exception # rubocop:disable Lint/RescueException
145
145
  # Swallow all errors including thread kill
146
+ ensure
147
+ close_http
146
148
  end
147
149
 
148
150
  def rate_limited?
@@ -188,16 +190,14 @@ module OpenTrace
188
190
  end
189
191
 
190
192
  def pop_with_timeout(timeout)
191
- deadline = Time.now + [timeout, 0].max
192
- loop do
193
- begin
194
- return @queue.pop(true)
195
- rescue ThreadError
196
- return nil if Time.now >= deadline || @queue.closed?
197
- sleep(POLL_INTERVAL)
198
- end
193
+ if timeout <= 0
194
+ # Deadline already passed — still try a non-blocking pop in case
195
+ # items arrived while we were busy (e.g. during version check).
196
+ @queue.pop(true)
197
+ else
198
+ @queue.pop(timeout: timeout)
199
199
  end
200
- rescue ClosedQueueError
200
+ rescue ThreadError, ClosedQueueError
201
201
  nil
202
202
  end
203
203
 
@@ -330,7 +330,6 @@ module OpenTrace
330
330
  end
331
331
 
332
332
  def http_post(json, batch_id: nil)
333
- http = build_http(@uri)
334
333
  request = Net::HTTP::Post.new(@uri.request_uri)
335
334
  request["Authorization"] = "Bearer #{@config.api_key}"
336
335
  request["Content-Type"] = "application/json"
@@ -345,7 +344,11 @@ module OpenTrace
345
344
  request.body = json
346
345
  end
347
346
 
348
- http.request(request)
347
+ persistent_http.request(request)
348
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Net::OpenTimeout => _e
349
+ # Connection went stale — reset and retry once
350
+ close_http
351
+ persistent_http.request(request)
349
352
  end
350
353
 
351
354
  def retryable_response?(response)
@@ -359,13 +362,27 @@ module OpenTrace
359
362
  delay + jitter
360
363
  end
361
364
 
362
- def build_http(uri)
363
- http = Net::HTTP.new(uri.host, uri.port)
364
- http.use_ssl = (uri.scheme == "https")
365
- http.open_timeout = @config.timeout
366
- http.read_timeout = @config.timeout
367
- http.write_timeout = @config.timeout
368
- http
365
+ # Returns a persistent Net::HTTP connection, creating one if needed.
366
+ # Only called from the dispatch thread — no synchronization needed.
367
+ def persistent_http
368
+ return @http if @http&.started?
369
+
370
+ @http = Net::HTTP.new(@uri.host, @uri.port)
371
+ @http.use_ssl = (@uri.scheme == "https")
372
+ @http.open_timeout = @config.timeout
373
+ @http.read_timeout = @config.timeout
374
+ @http.write_timeout = @config.timeout
375
+ @http.keep_alive_timeout = 30
376
+ @http.start
377
+ @http
378
+ end
379
+
380
+ def close_http
381
+ @http&.finish
382
+ rescue IOError
383
+ # Already closed
384
+ ensure
385
+ @http = nil
369
386
  end
370
387
 
371
388
  def gzip_compress(string)
@@ -421,13 +438,20 @@ module OpenTrace
421
438
  end
422
439
 
423
440
  @compatibility_checked = true
424
- rescue StandardError
425
- # Server might not support /api/version yet — that's fine
441
+ rescue Exception # rubocop:disable Lint/RescueException
442
+ # Server might not support /api/version yet — that's fine.
443
+ # Broad rescue: this is best-effort and must never kill the dispatch loop.
426
444
  @compatibility_checked = true
427
445
  end
428
446
 
447
+ # One-shot GET for version check. Uses a throwaway connection
448
+ # so a failure doesn't poison the persistent connection.
429
449
  def http_get(uri)
430
- http = build_http(uri)
450
+ http = Net::HTTP.new(uri.host, uri.port)
451
+ http.use_ssl = (uri.scheme == "https")
452
+ http.open_timeout = @config.timeout
453
+ http.read_timeout = @config.timeout
454
+ http.write_timeout = @config.timeout
431
455
  request = Net::HTTP::Get.new(uri.request_uri)
432
456
  request["User-Agent"] = "opentrace-ruby/#{OpenTrace::VERSION}"
433
457
  request["Authorization"] = "Bearer #{@config.api_key}"
@@ -20,7 +20,9 @@ module OpenTrace
20
20
  :request_summary, :timeline, :timeline_max_events,
21
21
  :memory_tracking, :http_tracking,
22
22
  :max_payload_bytes,
23
- :trace_propagation
23
+ :trace_propagation,
24
+ :log_forwarding, :view_tracking, :cache_tracking,
25
+ :deprecation_tracking, :detailed_request_log
24
26
 
25
27
  def initialize
26
28
  @endpoint = nil
@@ -30,7 +32,7 @@ module OpenTrace
30
32
  @timeout = 1.0
31
33
  @enabled = true
32
34
  @context = nil # nil | Hash | Proc
33
- @min_level = :debug # send everything by default
35
+ @min_level = :info
34
36
  @allowed_levels = nil # nil = use min_level threshold (backward compatible)
35
37
  @hostname = nil
36
38
  @pid = nil
@@ -46,7 +48,7 @@ module OpenTrace
46
48
  @on_drop = nil # ->(count, reason) { ... }
47
49
  @compression = true
48
50
  @compression_threshold = 1024 # only compress payloads > 1KB
49
- @sql_logging = true
51
+ @sql_logging = false
50
52
  @sql_duration_threshold_ms = 0.0
51
53
  @ignore_paths = []
52
54
  @pool_monitoring = false
@@ -54,12 +56,17 @@ module OpenTrace
54
56
  @queue_monitoring = false
55
57
  @queue_monitoring_interval = 60
56
58
  @request_summary = true
57
- @timeline = true
59
+ @timeline = false
58
60
  @timeline_max_events = 200
59
61
  @memory_tracking = false
60
62
  @http_tracking = false
61
63
  @max_payload_bytes = 262_144 # 256 KB
62
64
  @trace_propagation = true
65
+ @log_forwarding = false
66
+ @view_tracking = false
67
+ @cache_tracking = false
68
+ @deprecation_tracking = false
69
+ @detailed_request_log = false
63
70
  end
64
71
 
65
72
  def valid?
@@ -10,6 +10,9 @@ module OpenTrace
10
10
  end
11
11
 
12
12
  def call(env)
13
+ # When OpenTrace is disabled, pass through with zero overhead
14
+ return @app.call(env) unless OpenTrace.enabled?
15
+
13
16
  request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
14
17
  OpenTrace.current_request_id = request_id
15
18
  Fiber[:opentrace_sql_count] = 0
@@ -25,15 +28,18 @@ module OpenTrace
25
28
  Fiber[:opentrace_parent_span_id] = parent_span_id
26
29
  end
27
30
 
28
- # Create RequestCollector for accumulate-and-summarize pattern
29
- if OpenTrace.enabled? && OpenTrace.config.request_summary
30
- collector = OpenTrace::RequestCollector.new(
31
- max_timeline: OpenTrace.config.timeline_max_events
32
- )
31
+ # Create RequestCollector only when features that need it are enabled
32
+ cfg = OpenTrace.config
33
+ needs_collector = cfg.request_summary &&
34
+ (cfg.view_tracking || cfg.cache_tracking || cfg.http_tracking ||
35
+ cfg.timeline || cfg.memory_tracking)
36
+
37
+ if needs_collector
38
+ max_timeline = cfg.timeline ? cfg.timeline_max_events : 0
39
+ collector = OpenTrace::RequestCollector.new(max_timeline: max_timeline)
33
40
  Fiber[:opentrace_collector] = collector
34
41
 
35
- # Memory snapshot before request (opt-in)
36
- if OpenTrace.config.memory_tracking
42
+ if cfg.memory_tracking
37
43
  collector.memory_before = current_rss_mb
38
44
  end
39
45
  end
@@ -13,140 +13,111 @@ if defined?(::Rails::Railtie)
13
13
  config.after_initialize do |app|
14
14
  next unless OpenTrace.enabled?
15
15
 
16
- if Rails.logger.respond_to?(:broadcast_to)
17
- # Rails 7.1+: register as a broadcast target (non-invasive)
18
- Rails.logger.broadcast_to(OpenTrace::LogForwarder.new)
19
- else
20
- # Pre-7.1 fallback: wrap the logger directly
21
- if app.config.logger
22
- app.config.logger = OpenTrace::Logger.new(app.config.logger)
23
- Rails.logger = app.config.logger
24
- elsif Rails.logger
25
- Rails.logger = OpenTrace::Logger.new(Rails.logger)
16
+ # Log forwarding (opt-in) — registers LogForwarder/Logger wrapper
17
+ if OpenTrace.config.log_forwarding
18
+ if Rails.logger.respond_to?(:broadcast_to)
19
+ Rails.logger.broadcast_to(OpenTrace::LogForwarder.new)
20
+ else
21
+ if app.config.logger
22
+ app.config.logger = OpenTrace::Logger.new(app.config.logger)
23
+ Rails.logger = app.config.logger
24
+ elsif Rails.logger
25
+ Rails.logger = OpenTrace::Logger.new(Rails.logger)
26
+ end
26
27
  end
27
28
  end
28
29
 
29
- # Subscribe to controller request notifications
30
- ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
31
- event = ActiveSupport::Notifications::Event.new(*args)
32
- forward_request_log(event)
30
+ # Subscribe to controller request notifications — always on
31
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, started, finished, id, payload|
32
+ forward_request_log(name, started, finished, id, payload)
33
33
  rescue StandardError
34
34
  # Swallow - never affect the host app
35
35
  end
36
36
 
37
- # Subscribe to SQL query notifications (also increments N+1 counter and feeds collector)
38
- if OpenTrace.config.sql_logging
39
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
40
- # Skip Event allocation when outside a request and below threshold
41
- sql_count = Fiber[:opentrace_sql_count]
42
- collector = Fiber[:opentrace_collector]
43
- needs_forward = OpenTrace.config.sql_duration_threshold_ms <= 0
44
-
45
- next unless sql_count || collector || needs_forward
37
+ # SQL subscriber lightweight counter + optional forwarding
38
+ sql_logging = OpenTrace.config.sql_logging
39
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |name, started, finished, id, payload|
40
+ sql_count = Fiber[:opentrace_sql_count]
41
+ collector = Fiber[:opentrace_collector]
46
42
 
47
- event = ActiveSupport::Notifications::Event.new(*args)
43
+ if sql_count || collector || sql_logging
44
+ duration_ms = (finished && started) ? (finished - started) * 1000.0 : 0.0
48
45
 
49
- # Increment per-request SQL counter (Fiber-local, zero-cost)
46
+ # Increment per-request SQL counter (~1-5μs)
50
47
  if sql_count
51
48
  Fiber[:opentrace_sql_count] = sql_count + 1
52
- Fiber[:opentrace_sql_total_ms] = (Fiber[:opentrace_sql_total_ms] || 0.0) + (event.duration || 0.0)
49
+ Fiber[:opentrace_sql_total_ms] = (Fiber[:opentrace_sql_total_ms] || 0.0) + duration_ms
53
50
  end
54
51
 
55
- # Feed RequestCollector for timeline & summary
52
+ # Feed RequestCollector for summary (no table extraction on hot path)
56
53
  if collector
57
- payload = event.payload
58
54
  unless payload[:name] == "SCHEMA"
59
- table = nil
60
- if payload[:sql] =~ /\b(?:FROM|INTO|UPDATE|JOIN)\s+[`"]?(\w+)[`"]?/i
61
- table = $1
62
- end
63
- collector.record_sql(name: payload[:name], duration_ms: event.duration || 0.0, table: table)
55
+ collector.record_sql(name: payload[:name], duration_ms: duration_ms)
64
56
  end
65
57
  end
66
58
 
67
- forward_sql_log(event)
68
- rescue StandardError
69
- # Swallow
70
- end
71
- else
72
- # Even when sql_logging is off, still count queries for N+1 detection and feed collector
73
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
74
- sql_count = Fiber[:opentrace_sql_count]
75
- collector = Fiber[:opentrace_collector]
76
- next unless sql_count || collector
77
-
78
- event = ActiveSupport::Notifications::Event.new(*args)
79
-
80
- if sql_count
81
- Fiber[:opentrace_sql_count] = sql_count + 1
82
- Fiber[:opentrace_sql_total_ms] = (Fiber[:opentrace_sql_total_ms] || 0.0) + (event.duration || 0.0)
83
- end
84
-
85
- # Feed RequestCollector for timeline & summary
86
- if collector
87
- payload = event.payload
88
- unless payload[:name] == "SCHEMA"
89
- table = nil
90
- if payload[:sql] =~ /\b(?:FROM|INTO|UPDATE|JOIN)\s+[`"]?(\w+)[`"]?/i
91
- table = $1
92
- end
93
- collector.record_sql(name: payload[:name], duration_ms: event.duration || 0.0, table: table)
94
- end
59
+ # Forward individual SQL log (opt-in)
60
+ if sql_logging
61
+ forward_sql_log(payload, duration_ms)
95
62
  end
96
- rescue StandardError
97
- # Swallow
98
63
  end
99
- end
100
-
101
- # Subscribe to ActiveJob notifications
102
- ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
103
- event = ActiveSupport::Notifications::Event.new(*args)
104
- forward_job_log(event)
105
64
  rescue StandardError
106
65
  # Swallow
107
66
  end
108
67
 
109
- # Subscribe to deprecation warnings
110
- ActiveSupport::Notifications.subscribe("deprecation.rails") do |*args|
68
+ # Subscribe to ActiveJob notifications — always on
69
+ ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
111
70
  event = ActiveSupport::Notifications::Event.new(*args)
112
- forward_deprecation_log(event)
71
+ forward_job_log(event)
113
72
  rescue StandardError
114
73
  # Swallow
115
74
  end
116
75
 
117
- # View render tracking (only records when RequestCollector exists)
118
- %w[render_template.action_view render_partial.action_view].each do |event_name|
119
- ActiveSupport::Notifications.subscribe(event_name) do |*args|
120
- collector = Fiber[:opentrace_collector]
121
- next unless collector
122
-
123
- event = ActiveSupport::Notifications::Event.new(*args)
124
- template = event.payload[:identifier]
125
- # Shorten: /Users/deploy/app/views/orders/show.html.erb → orders/show.html.erb
126
- template = template.split("views/").last if template&.include?("views/")
127
-
128
- collector.record_view(template: template, duration_ms: event.duration || 0.0)
76
+ # Deprecation warnings (opt-in)
77
+ if OpenTrace.config.deprecation_tracking
78
+ ActiveSupport::Notifications.subscribe("deprecation.rails") do |_name, _started, _finished, _id, payload|
79
+ forward_deprecation_log(payload)
129
80
  rescue StandardError
130
81
  # Swallow
131
82
  end
132
83
  end
133
84
 
134
- # Cache operation tracking (only records when RequestCollector exists)
135
- %w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
136
- ActiveSupport::Notifications.subscribe(event_name) do |*args|
137
- collector = Fiber[:opentrace_collector]
138
- next unless collector
85
+ # View render tracking (opt-in)
86
+ if OpenTrace.config.view_tracking
87
+ %w[render_template.action_view render_partial.action_view].each do |event_name|
88
+ ActiveSupport::Notifications.subscribe(event_name) do |_name, started, finished, _id, payload|
89
+ collector = Fiber[:opentrace_collector]
90
+ next unless collector
139
91
 
140
- event = ActiveSupport::Notifications::Event.new(*args)
141
- action = event_name.split(".").first.sub("cache_", "").to_sym # :read, :write, :delete
92
+ duration_ms = (finished && started) ? (finished - started) * 1000.0 : 0.0
93
+ template = payload[:identifier]
94
+ template = template.split("views/").last if template&.include?("views/")
142
95
 
143
- collector.record_cache(
144
- action: action,
145
- hit: event.payload[:hit],
146
- duration_ms: event.duration || 0.0
147
- )
148
- rescue StandardError
149
- # Swallow
96
+ collector.record_view(template: template, duration_ms: duration_ms)
97
+ rescue StandardError
98
+ # Swallow
99
+ end
100
+ end
101
+ end
102
+
103
+ # Cache operation tracking (opt-in)
104
+ if OpenTrace.config.cache_tracking
105
+ %w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
106
+ ActiveSupport::Notifications.subscribe(event_name) do |_name, started, finished, _id, payload|
107
+ collector = Fiber[:opentrace_collector]
108
+ next unless collector
109
+
110
+ duration_ms = (finished && started) ? (finished - started) * 1000.0 : 0.0
111
+ action = event_name.split(".").first.sub("cache_", "").to_sym
112
+
113
+ collector.record_cache(
114
+ action: action,
115
+ hit: payload[:hit],
116
+ duration_ms: duration_ms
117
+ )
118
+ rescue StandardError
119
+ # Swallow
120
+ end
150
121
  end
151
122
  end
152
123
 
@@ -177,20 +148,21 @@ if defined?(::Rails::Railtie)
177
148
  class << self
178
149
  private
179
150
 
180
- def forward_request_log(event)
151
+ def forward_request_log(_name, started, finished, _id, payload)
181
152
  return unless OpenTrace.enabled?
182
-
183
- payload = event.payload
184
153
  return if ignored_path?(payload[:path])
154
+
155
+ duration_ms = (finished && started) ? (finished - started) * 1000.0 : 0.0
156
+
185
157
  metadata = {
186
158
  request_id: payload[:headers]&.env&.dig("action_dispatch.request_id")
187
159
  }.compact
188
160
 
189
- # Attempt to capture current user ID if available
190
- user_id = extract_user_id(payload)
161
+ # User ID from cached context only (never calls controller.current_user)
162
+ user_id = extract_user_id
191
163
  metadata[:user_id] = user_id if user_id
192
164
 
193
- # Exception auto-capture with fingerprinting
165
+ # Exception auto-capture with fingerprinting (always on)
194
166
  if payload[:exception]
195
167
  metadata[:exception_class] = payload[:exception][0]
196
168
  metadata[:exception_message] = truncate(payload[:exception][1], 500)
@@ -203,11 +175,11 @@ if defined?(::Rails::Railtie)
203
175
  payload[:exception][0], cleaned)
204
176
  end
205
177
 
206
- # Filtered request params
207
- extract_params(payload, metadata)
208
-
209
- # Request headers
210
- extract_request_headers(payload, metadata)
178
+ # Detailed request info (opt-in)
179
+ if OpenTrace.config.detailed_request_log
180
+ extract_params(payload, metadata)
181
+ extract_request_headers(payload, metadata)
182
+ end
211
183
 
212
184
  # Build structured request_summary from collector (or fall back to Fiber-locals)
213
185
  request_summary = nil
@@ -220,7 +192,7 @@ if defined?(::Rails::Railtie)
220
192
  method: payload[:method],
221
193
  path: payload[:path],
222
194
  status: payload[:status],
223
- duration_ms: event.duration&.round(1),
195
+ duration_ms: duration_ms.round(1),
224
196
  sql_count: summary[:sql_query_count],
225
197
  sql_total_ms: summary[:sql_total_ms],
226
198
  sql_slowest_ms: summary[:sql_slowest_ms],
@@ -245,11 +217,10 @@ if defined?(::Rails::Railtie)
245
217
  }.compact
246
218
 
247
219
  # Compute time breakdown
248
- total = event.duration || 0.0
249
- if total > 0
250
- sql_pct = [((collector.sql_total_ms / total) * 100).round(1), 100.0].min
251
- view_pct = [((collector.view_total_ms / total) * 100).round(1), 100.0].min
252
- http_pct = collector.http_count > 0 ? [((collector.http_total_ms / total) * 100).round(1), 100.0].min : 0.0
220
+ if duration_ms > 0
221
+ sql_pct = [((collector.sql_total_ms / duration_ms) * 100).round(1), 100.0].min
222
+ view_pct = [((collector.view_total_ms / duration_ms) * 100).round(1), 100.0].min
223
+ http_pct = collector.http_count > 0 ? [((collector.http_total_ms / duration_ms) * 100).round(1), 100.0].min : 0.0
253
224
  other_pct = [100 - sql_pct - view_pct - http_pct, 0].max.round(1)
254
225
  request_summary[:time_breakdown] = {
255
226
  sql_pct: sql_pct,
@@ -265,7 +236,7 @@ if defined?(::Rails::Railtie)
265
236
  metadata[:method] = payload[:method]
266
237
  metadata[:path] = payload[:path]
267
238
  metadata[:status] = payload[:status]
268
- metadata[:duration_ms] = event.duration&.round(1)
239
+ metadata[:duration_ms] = duration_ms.round(1)
269
240
  metadata[:sql_query_count] = Fiber[:opentrace_sql_count]
270
241
  metadata[:sql_total_ms] = Fiber[:opentrace_sql_total_ms]&.round(1)
271
242
  metadata[:n_plus_one_warning] = true if Fiber[:opentrace_sql_count] > 20
@@ -276,7 +247,7 @@ if defined?(::Rails::Railtie)
276
247
  metadata[:method] = payload[:method]
277
248
  metadata[:path] = payload[:path]
278
249
  metadata[:status] = payload[:status]
279
- metadata[:duration_ms] = event.duration&.round(1)
250
+ metadata[:duration_ms] = duration_ms.round(1)
280
251
  end
281
252
 
282
253
  level = if payload[:exception]
@@ -288,7 +259,7 @@ if defined?(::Rails::Railtie)
288
259
  else
289
260
  "INFO"
290
261
  end
291
- message = "#{payload[:method]} #{payload[:path]} #{payload[:status]} #{event.duration&.round(1)}ms"
262
+ message = "#{payload[:method]} #{payload[:path]} #{payload[:status]} #{duration_ms.round(1)}ms"
292
263
 
293
264
  OpenTrace.log(level, message, metadata, request_summary: request_summary)
294
265
  rescue StandardError
@@ -356,11 +327,10 @@ if defined?(::Rails::Railtie)
356
327
  # Swallow
357
328
  end
358
329
 
359
- def forward_sql_log(event)
330
+ def forward_sql_log(payload, duration_ms)
360
331
  return unless OpenTrace.enabled?
361
332
 
362
- payload = event.payload
363
- duration = event.duration&.round(2)
333
+ duration = duration_ms&.round(2)
364
334
  threshold = OpenTrace.config.sql_duration_threshold_ms
365
335
 
366
336
  # Skip if below threshold
@@ -389,10 +359,9 @@ if defined?(::Rails::Railtie)
389
359
  # Swallow
390
360
  end
391
361
 
392
- def forward_deprecation_log(event)
362
+ def forward_deprecation_log(payload)
393
363
  return unless OpenTrace.enabled?
394
364
 
395
- payload = event.payload
396
365
  message = payload[:message].to_s
397
366
  callsite = payload[:callstack]&.first&.to_s
398
367
 
@@ -423,19 +392,9 @@ if defined?(::Rails::Railtie)
423
392
  # Swallow
424
393
  end
425
394
 
426
- def extract_user_id(payload)
427
- # Prefer user_id from cached request context (avoids calling
428
- # current_user which may trigger DB queries or auth logic)
395
+ def extract_user_id
429
396
  cached = Fiber[:opentrace_cached_context]
430
- return cached[:user_id] if cached&.key?(:user_id)
431
-
432
- controller = payload[:controller_instance]
433
- return unless controller
434
-
435
- if controller.respond_to?(:current_user, true)
436
- user = controller.send(:current_user)
437
- user.respond_to?(:id) ? user.id : nil
438
- end
397
+ cached[:user_id] if cached&.key?(:user_id)
439
398
  rescue StandardError
440
399
  nil
441
400
  end
@@ -12,6 +12,7 @@ module OpenTrace
12
12
 
13
13
  def initialize(max_timeline: MAX_TIMELINE_EVENTS)
14
14
  @max_timeline = max_timeline
15
+ @timeline_enabled = max_timeline > 0
15
16
 
16
17
  @sql_count = 0
17
18
  @sql_total_ms = 0.0
@@ -36,8 +37,12 @@ module OpenTrace
36
37
  @memory_before = nil
37
38
  @memory_after = nil
38
39
 
39
- @timeline = []
40
- @request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
40
+ if @timeline_enabled
41
+ @timeline = []
42
+ @request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
+ else
44
+ @timeline = nil
45
+ end
41
46
  end
42
47
 
43
48
  def record_sql(name:, duration_ms:, table: nil)
@@ -49,7 +54,9 @@ module OpenTrace
49
54
  @sql_slowest_name = name
50
55
  end
51
56
 
52
- append_timeline({ t: :sql, n: name, ms: duration_ms.round(1), at: offset_ms })
57
+ if @timeline_enabled
58
+ append_timeline({ t: :sql, n: name, ms: duration_ms.round(1), at: offset_ms })
59
+ end
53
60
  end
54
61
 
55
62
  def record_view(template:, duration_ms:)
@@ -61,7 +68,9 @@ module OpenTrace
61
68
  @view_slowest_template = template
62
69
  end
63
70
 
64
- append_timeline({ t: :view, n: template, ms: duration_ms.round(1), at: offset_ms })
71
+ if @timeline_enabled
72
+ append_timeline({ t: :view, n: template, ms: duration_ms.round(1), at: offset_ms })
73
+ end
65
74
  end
66
75
 
67
76
  def record_cache(action:, hit: nil, duration_ms: 0.0)
@@ -75,7 +84,9 @@ module OpenTrace
75
84
  @cache_deletes += 1
76
85
  end
77
86
 
78
- append_timeline({ t: :cache, a: action, hit: hit, ms: duration_ms.round(2), at: offset_ms })
87
+ if @timeline_enabled
88
+ append_timeline({ t: :cache, a: action, hit: hit, ms: duration_ms.round(2), at: offset_ms })
89
+ end
79
90
  end
80
91
 
81
92
  def record_http(method:, url:, host:, status:, duration_ms:, error: nil)
@@ -87,9 +98,11 @@ module OpenTrace
87
98
  @http_slowest_host = host
88
99
  end
89
100
 
90
- entry = { t: :http, n: "#{method} #{host}", ms: duration_ms.round(1), s: status, at: offset_ms }
91
- entry[:err] = error if error
92
- append_timeline(entry)
101
+ if @timeline_enabled
102
+ entry = { t: :http, n: "#{method} #{host}", ms: duration_ms.round(1), s: status, at: offset_ms }
103
+ entry[:err] = error if error
104
+ append_timeline(entry)
105
+ end
93
106
  end
94
107
 
95
108
  def summary
@@ -107,7 +120,7 @@ module OpenTrace
107
120
  cache_writes: @cache_writes,
108
121
  cache_hit_ratio: @cache_reads > 0 ? (@cache_hits.to_f / @cache_reads).round(2) : nil,
109
122
  n_plus_one_warning: @sql_count > 20 ? true : nil,
110
- timeline: @timeline.empty? ? nil : @timeline
123
+ timeline: (@timeline.nil? || @timeline.empty?) ? nil : @timeline
111
124
  }
112
125
 
113
126
  # HTTP stats (only present if calls were made)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenTrace
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opentrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenTrace