opentrace 0.17.1 → 0.17.3

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: a52fdd745f23752d86ae494048fb8b6395374ec08f7ba6b9e3edccd0bea80036
4
- data.tar.gz: b9b3aaab811c9997681a535a1b50d7c9ad0e567c872cb0075c509abd7567f339
3
+ metadata.gz: f2ea4cd5b52e24bbbed81f9d9c9f522f8001bf379407cd01c126b659d0a4efe5
4
+ data.tar.gz: 52cb261f68c4e1280b1c164fb732e9a02123abd70a0274e70ed8894af76b9c9b
5
5
  SHA512:
6
- metadata.gz: 7dc954766e198906d4c16448b49be2e0dc26aca08e5f8fa3031d3a6f6c6d9c98f6278e2febbc3f717680522b06807c756a80f3b84dbf69c1c8c20e8ca1c99b6b
7
- data.tar.gz: 0f0a49079c6bef6317208325361dfa4edd07cc509043a7e10dbceadd67ecda35311e306c2e5fb64115bf4e6fab111e23fac05ccbe5bcddb5324aad7325d4a06c
6
+ metadata.gz: 1b5cbf4a21b4e09f9246cfa89e8d45f21f6b7b6ad4d232a2b909a8b76fd9ce6829cee208b8bfec596198954de50574a52d249dc63761c8be3e394640f3885b9d
7
+ data.tar.gz: d7714397187d069769743c30d39a3ccf865624c40ba86d6f3c9a0fad5bb56f10417660045eb4c07123dd0260359b104f39fd2449beb7371f32512836cd585257
@@ -6,6 +6,7 @@ require "json"
6
6
  require "zlib"
7
7
  require "stringio"
8
8
  require "securerandom"
9
+ require_relative "serializer"
9
10
 
10
11
  module OpenTrace
11
12
  class Client
@@ -20,6 +21,8 @@ module OpenTrace
20
21
  @config = config
21
22
  @sampler = sampler
22
23
  @queue = Thread::Queue.new
24
+ @queue_bytes = 0
25
+ @queue_bytes_mutex = Mutex.new
23
26
  @mutex = Mutex.new
24
27
  @thread = nil
25
28
  @pid = Process.pid
@@ -46,14 +49,13 @@ module OpenTrace
46
49
 
47
50
  reset_after_fork! if forked?
48
51
 
49
- # Drop newest if queue is full
50
- if @queue.size >= MAX_QUEUE_SIZE
52
+ byte_size = estimate_payload_bytes(payload)
53
+ unless push_queue_item(payload, byte_size: byte_size)
51
54
  @stats.increment(:dropped_queue_full)
52
55
  fire_on_drop(1, :queue_full)
53
56
  return
54
57
  end
55
58
 
56
- @queue.push(payload)
57
59
  @stats.increment(:enqueued)
58
60
  ensure_thread_running
59
61
  end
@@ -62,6 +64,10 @@ module OpenTrace
62
64
  @queue.size
63
65
  end
64
66
 
67
+ def queue_byte_size
68
+ @queue_bytes_mutex.synchronize { @queue_bytes }
69
+ end
70
+
65
71
  def circuit_state
66
72
  @circuit_breaker.state
67
73
  end
@@ -73,6 +79,7 @@ module OpenTrace
73
79
  def stats_snapshot
74
80
  @stats.to_h.merge(
75
81
  queue_size: queue_size,
82
+ queue_byte_size: queue_byte_size,
76
83
  circuit_state: circuit_state,
77
84
  auth_suspended: @auth_suspended,
78
85
  server_capabilities: @server_capabilities
@@ -99,6 +106,8 @@ module OpenTrace
99
106
  # Re-create everything cleanly in the child process.
100
107
  @pid = Process.pid
101
108
  @queue = Thread::Queue.new
109
+ @queue_bytes = 0
110
+ @queue_bytes_mutex = Mutex.new
102
111
  @mutex = Mutex.new
103
112
  @thread = nil
104
113
  @http = nil # Parent's connection is unusable after fork
@@ -178,8 +187,8 @@ module OpenTrace
178
187
  # Non-blocking drain: grab everything currently in the queue
179
188
  while batch.size < @config.batch_size
180
189
  begin
181
- item = @queue.pop(true) # non_block = true
182
- batch << item
190
+ item = unwrap_queue_item(@queue.pop(true)) # non_block = true
191
+ batch << item if item
183
192
  rescue ThreadError, ClosedQueueError
184
193
  break # queue empty or closed
185
194
  end
@@ -216,9 +225,9 @@ module OpenTrace
216
225
  if timeout <= 0
217
226
  # Deadline already passed — still try a non-blocking pop in case
218
227
  # items arrived while we were busy (e.g. during version check).
219
- @queue.pop(true)
228
+ unwrap_queue_item(@queue.pop(true))
220
229
  else
221
- @queue.pop(timeout: [timeout, MAX_POP_WAIT].min)
230
+ unwrap_queue_item(@queue.pop(timeout: [timeout, MAX_POP_WAIT].min))
222
231
  end
223
232
  rescue ThreadError, ClosedQueueError
224
233
  nil
@@ -324,9 +333,8 @@ module OpenTrace
324
333
  # Re-enqueue batch items if space allows
325
334
  re_enqueued = 0
326
335
  batch.each do |payload|
327
- break if @queue.size >= MAX_QUEUE_SIZE
328
336
  begin
329
- @queue.push(payload)
337
+ break unless push_queue_item(payload)
330
338
  re_enqueued += 1
331
339
  rescue ClosedQueueError
332
340
  break
@@ -504,6 +512,50 @@ module OpenTrace
504
512
  PiiScrubber::PATTERNS.values
505
513
  end
506
514
 
515
+ def push_queue_item(payload, byte_size: nil)
516
+ byte_size ||= estimate_payload_bytes(payload)
517
+
518
+ @queue_bytes_mutex.synchronize do
519
+ return false if @queue.closed?
520
+ return false if @queue.size >= MAX_QUEUE_SIZE
521
+ return false if @config.max_queue_bytes && @queue_bytes + byte_size > @config.max_queue_bytes
522
+
523
+ @queue.push([payload, byte_size])
524
+ @queue_bytes += byte_size
525
+ true
526
+ end
527
+ rescue ClosedQueueError
528
+ false
529
+ end
530
+
531
+ def unwrap_queue_item(item)
532
+ return nil unless item
533
+
534
+ if item.is_a?(Array) && item.length == 2 && item[1].is_a?(Integer)
535
+ payload, byte_size = item
536
+ decrement_queue_bytes(byte_size)
537
+ payload
538
+ else
539
+ item
540
+ end
541
+ end
542
+
543
+ def decrement_queue_bytes(byte_size)
544
+ @queue_bytes_mutex.synchronize do
545
+ @queue_bytes = [0, @queue_bytes - byte_size.to_i].max
546
+ end
547
+ end
548
+
549
+ def estimate_payload_bytes(payload)
550
+ if defined?(Serializer)
551
+ Serializer.estimate_size(payload)
552
+ else
553
+ JSON.generate(payload).bytesize
554
+ end
555
+ rescue StandardError
556
+ 1024
557
+ end
558
+
507
559
  def fire_on_drop(count, reason)
508
560
  @config.on_drop&.call(count, reason)
509
561
  rescue StandardError
@@ -3,6 +3,7 @@
3
3
  require_relative "buffer_pool"
4
4
  require_relative "memory_guard"
5
5
  require_relative "capture_rules"
6
+ require_relative "config"
6
7
 
7
8
  module OpenTrace
8
9
  class InstrumentationContext
@@ -20,6 +21,12 @@ module OpenTrace
20
21
  # Returns the buffer (for callers that need it).
21
22
  def setup(env: nil, job: nil)
22
23
  buf = buffer_pool.checkout
24
+ allocation_bytes = buf.byte_size
25
+
26
+ unless memory_guard.allocate(allocation_bytes)
27
+ buffer_pool.checkin(buf)
28
+ return nil
29
+ end
23
30
 
24
31
  buf.event_type = if env
25
32
  :http_request
@@ -27,6 +34,7 @@ module OpenTrace
27
34
  :job_perform
28
35
  end
29
36
 
37
+ Fiber[:opentrace_buffer_allocation_bytes] = allocation_bytes
30
38
  Fiber[FIBER_KEY] = buf
31
39
  buf
32
40
  end
@@ -38,34 +46,40 @@ module OpenTrace
38
46
  # document, checks the buffer back into the pool, and clears the Fiber
39
47
  # local.
40
48
  #
41
- # Returns the document Hash. The caller is responsible for enqueueing.
49
+ # Returns the document Hash, or nil when capture resolves to :none.
50
+ # The caller is responsible for enqueueing.
42
51
  def teardown(status: nil, duration_ms: nil, error: false)
43
52
  buf = Fiber[FIBER_KEY]
44
53
  return nil unless buf
45
54
 
46
- # Resolve capture level
47
- capture_level = resolve_capture_level(
48
- buf, status: status, duration_ms: duration_ms, error: error
49
- )
50
-
51
- # Apply memory guard — may downgrade under pressure
52
- capture_level = memory_guard.effective_level(capture_level)
53
-
54
- # Normalize: MemoryGuard returns :none when exceeded, but RequestBuffer
55
- # only understands :minimal / :standard / :full. Map :none to :minimal.
56
- capture_level = :minimal if capture_level == :none
57
-
58
- # Build domain overrides from capture rules (if configured)
59
- domain_overrides = {}
60
-
61
- # Produce the document
62
- doc = buf.to_document(capture_level: capture_level, domain_overrides: domain_overrides)
63
-
64
- # Return buffer to pool and clear Fiber local
65
- buffer_pool.checkin(buf)
66
- Fiber[FIBER_KEY] = nil
67
-
68
- doc
55
+ begin
56
+ # Resolve capture level
57
+ capture_level = resolve_capture_level(
58
+ buf, status: status, duration_ms: duration_ms, error: error
59
+ )
60
+
61
+ # Apply memory guard — may downgrade under pressure
62
+ capture_level = memory_guard.effective_level(capture_level)
63
+ return nil if capture_level == :none
64
+
65
+ # Build domain overrides from capture rules (if configured)
66
+ domain_overrides = configured_domain_overrides
67
+
68
+ # Produce the document
69
+ doc = buf.to_document(capture_level: capture_level, domain_overrides: domain_overrides)
70
+ doc[:duration_ms] = duration_ms if duration_ms
71
+ doc[:pending_explains] = Fiber[:opentrace_pending_explains] if Fiber[:opentrace_pending_explains]
72
+
73
+ doc
74
+ ensure
75
+ allocation_bytes = Fiber[:opentrace_buffer_allocation_bytes].to_i
76
+ memory_guard.release(allocation_bytes) if allocation_bytes.positive?
77
+
78
+ # Return buffer to pool and clear Fiber local
79
+ buffer_pool.checkin(buf)
80
+ Fiber[FIBER_KEY] = nil
81
+ Fiber[:opentrace_buffer_allocation_bytes] = nil
82
+ end
69
83
  end
70
84
 
71
85
  # ── Convenience accessors ──
@@ -83,15 +97,28 @@ module OpenTrace
83
97
  # ── Singleton resources (lazy-initialized) ──
84
98
 
85
99
  def buffer_pool
86
- @buffer_pool ||= BufferPool.new
100
+ cfg = active_config
101
+ @buffer_pool ||= BufferPool.new(
102
+ max_buffer_bytes: cfg.max_buffer_bytes,
103
+ max_audit_events: cfg.audit_max_events_per_request
104
+ )
87
105
  end
88
106
 
89
107
  def memory_guard
90
- @memory_guard ||= MemoryGuard.new
108
+ cfg = active_config
109
+ @memory_guard ||= MemoryGuard.new(max_total_bytes: cfg.max_total_buffer_bytes)
110
+ end
111
+
112
+ def configure!(config)
113
+ @config = config
114
+ @buffer_pool = nil
115
+ @memory_guard = nil
116
+ @capture_rules = config.capture_rules_block ? CaptureRules.new(&config.capture_rules_block) : nil
91
117
  end
92
118
 
93
119
  # Reset singletons (for testing).
94
120
  def reset!
121
+ @config = nil
95
122
  @buffer_pool = nil
96
123
  @memory_guard = nil
97
124
  @capture_rules = nil
@@ -106,9 +133,10 @@ module OpenTrace
106
133
  # If CaptureRules are configured, use them; otherwise default to :standard.
107
134
  def resolve_capture_level(buf, status:, duration_ms:, error:)
108
135
  rules = capture_rules
136
+ base_level = configured_capture_depth
109
137
 
110
138
  unless rules
111
- return :standard
139
+ return base_level
112
140
  end
113
141
 
114
142
  # CaptureRules.resolve expects a Rack env for path matching.
@@ -122,12 +150,36 @@ module OpenTrace
122
150
 
123
151
  rules.resolve(
124
152
  env,
125
- base_level: :standard,
153
+ base_level: base_level,
126
154
  status: status,
127
155
  duration_ms: duration_ms,
128
156
  error: error
129
157
  )
130
158
  end
159
+
160
+ def configured_capture_depth
161
+ level = active_config.capture_depth.to_s.downcase.to_sym
162
+ CaptureRules::LEVELS.include?(level) ? level : :standard
163
+ rescue StandardError
164
+ :standard
165
+ end
166
+
167
+ def configured_domain_overrides
168
+ cfg = active_config
169
+ {
170
+ email_capture: cfg.email_capture,
171
+ sql_capture: cfg.sql_capture,
172
+ http_capture: cfg.http_capture,
173
+ audit_capture: cfg.audit_capture,
174
+ request_capture: cfg.request_capture
175
+ }.compact
176
+ rescue StandardError
177
+ {}
178
+ end
179
+
180
+ def active_config
181
+ @config || (OpenTrace.respond_to?(:config) ? OpenTrace.config : Config.new)
182
+ end
131
183
  end
132
184
  end
133
185
  end
@@ -24,11 +24,32 @@ module OpenTrace
24
24
  end
25
25
 
26
26
  def add(severity, message = nil, progname = nil, &block)
27
- # Delegate to wrapped logger first (synchronous, as required)
28
- @wrapped_logger.add(severity, message, progname, &block)
27
+ if block && message.nil?
28
+ evaluated = false
29
+ resolved_message = nil
30
+ cached_block = proc do
31
+ unless evaluated
32
+ resolved_message = block.call
33
+ evaluated = true
34
+ end
35
+ resolved_message
36
+ end
37
+
38
+ # Delegate to wrapped logger first (synchronous, as required)
39
+ @wrapped_logger.add(severity, message, progname, &cached_block)
40
+
41
+ # If the wrapped logger filtered this level, Ruby Logger never
42
+ # evaluates the block. Match that behavior for forwarding.
43
+ return true unless evaluated
29
44
 
30
- # Forward to OpenTrace, never raise
31
- forward_to_opentrace(severity, message, progname, &block)
45
+ forward_to_opentrace(severity, resolved_message, nil)
46
+ else
47
+ # Delegate to wrapped logger first (synchronous, as required)
48
+ @wrapped_logger.add(severity, message, progname)
49
+
50
+ # Forward to OpenTrace, never raise
51
+ forward_to_opentrace(severity, message, progname)
52
+ end
32
53
 
33
54
  true
34
55
  rescue StandardError
@@ -21,6 +21,7 @@ module OpenTrace
21
21
  opentrace_session_id
22
22
  opentrace_pending_explains
23
23
  opentrace_buffer
24
+ opentrace_buffer_allocation_bytes
24
25
  ].freeze
25
26
 
26
27
  def initialize(app)
@@ -30,6 +31,7 @@ module OpenTrace
30
31
  def call(env)
31
32
  # When OpenTrace is disabled, pass through with zero overhead
32
33
  return @app.call(env) unless OpenTrace.enabled?
34
+ return @app.call(env) if ignored_path?(env["PATH_INFO"])
33
35
 
34
36
  # Sampling: skip ALL Fiber-local setup for unsampled requests.
35
37
  # Subscribers check Fiber-locals and return instantly when nil.
@@ -76,7 +78,7 @@ module OpenTrace
76
78
  end
77
79
 
78
80
  # ── Deep capture: set up InstrumentationContext ──
79
- buffer = setup_buffer(env, cfg)
81
+ buffer = setup_buffer(env, cfg) if request_capture_enabled?(cfg)
80
82
 
81
83
  # ── Call the downstream app ──
82
84
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -127,10 +129,32 @@ module OpenTrace
127
129
  FIBER_KEYS.each { |key| Fiber[key] = nil }
128
130
  end
129
131
 
132
+ def ignored_path?(path)
133
+ return false if path.nil?
134
+
135
+ OpenTrace.config.ignore_paths.any? do |entry|
136
+ entry.is_a?(Regexp) ? entry.match?(path) : path == entry
137
+ end
138
+ rescue StandardError
139
+ false
140
+ end
141
+
142
+ def request_capture_enabled?(cfg)
143
+ return false unless cfg.request_summary
144
+ return true if cfg.capture_rules_block
145
+ domain_overrides = [cfg.email_capture, cfg.sql_capture, cfg.http_capture, cfg.audit_capture, cfg.request_capture]
146
+ return true if domain_overrides.compact.any? { |level| level.to_s.downcase.to_sym != :none }
147
+
148
+ cfg.capture_depth.to_s.downcase.to_sym != :none
149
+ rescue StandardError
150
+ true
151
+ end
152
+
130
153
  # Set up the InstrumentationContext buffer and populate request fields.
131
154
  # Wrapped in rescue so deep capture failures never affect the host app.
132
155
  def setup_buffer(env, cfg)
133
156
  buffer = InstrumentationContext.setup(env: env)
157
+ return nil unless buffer
134
158
 
135
159
  buffer.request_method = env["REQUEST_METHOD"]
136
160
  buffer.request_path = env["PATH_INFO"]
@@ -172,28 +196,25 @@ module OpenTrace
172
196
  # Never affect the host app
173
197
  end
174
198
 
175
- # Capture response status, headers, and body into the buffer.
176
- # For non-streaming responses, the body content is saved.
177
- # For streaming responses, only Content-Length is tracked.
199
+ # Capture response status, headers, and size into the buffer.
200
+ #
201
+ # We intentionally do NOT call `body.to_ary` or `body.each` here. Per
202
+ # the Rack spec, a response body must be iterated exactly once — by the
203
+ # web server. Materialising it inside a middleware is a spec violation
204
+ # and can have surprising side-effects for wrappers that execute their
205
+ # source callable on iteration (observed in the wild with Falcon +
206
+ # async-job: calling `to_ary` here caused the downstream app to
207
+ # effectively run twice, duplicating ActiveJob enqueues).
208
+ #
209
+ # Size is read from the Content-Length header instead, which is the
210
+ # authoritative value for non-streaming responses and safely nil for
211
+ # streaming responses.
178
212
  def capture_response(buffer, status, headers, body)
179
213
  buffer.response_status = status
180
214
  buffer.response_headers = headers
181
215
 
182
- streaming = headers && (
183
- headers["Transfer-Encoding"] == "chunked" ||
184
- (headers.respond_to?(:key?) && !body.respond_to?(:to_ary))
185
- )
186
-
187
- if streaming
188
- # Streaming: only track size from Content-Length header
189
- buffer.response_size = headers["Content-Length"]&.to_i
190
- else
191
- # Non-streaming: collect body parts
192
- parts = body.respond_to?(:to_ary) ? body.to_ary : []
193
- collected = parts.join
194
- buffer.response_body = collected unless collected.empty?
195
- buffer.response_size = collected.bytesize
196
- end
216
+ cl = headers && (headers["Content-Length"] || headers["content-length"])
217
+ buffer.response_size = cl.to_i if cl
197
218
  rescue StandardError
198
219
  # Never affect the host app
199
220
  end
@@ -11,7 +11,11 @@ module OpenTrace
11
11
 
12
12
  def materialize(entry, config)
13
13
  if entry.is_a?(Array)
14
- entry[0] == :request ? materialize_request(entry, config) : materialize_log(entry, config)
14
+ case entry[0]
15
+ when :request then materialize_request(entry, config)
16
+ when :raw_document then materialize_raw_document(entry[1], config)
17
+ else materialize_log(entry, config)
18
+ end
15
19
  elsif entry.is_a?(Hash)
16
20
  entry # legacy direct payload
17
21
  end
@@ -93,6 +97,72 @@ module OpenTrace
93
97
  payload
94
98
  end
95
99
 
100
+ def materialize_raw_document(doc, config)
101
+ return nil unless doc.is_a?(Hash)
102
+
103
+ req = doc[:request] || {}
104
+ resp = doc[:response] || {}
105
+ duration_ms = if doc[:duration_ms]
106
+ doc[:duration_ms].to_f.round(0)
107
+ elsif doc[:started_at]
108
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - doc[:started_at]) * 1000).round(0)
109
+ else
110
+ 0
111
+ end
112
+
113
+ status = resp[:status] || resp[:response_status]
114
+ level = if status.to_i >= 500 then "error"
115
+ elsif status.to_i >= 400 then "warn"
116
+ else "info"
117
+ end
118
+
119
+ message = "#{req[:method]} #{req[:path]} #{status} #{duration_ms}ms"
120
+
121
+ payload = {
122
+ ts: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ"),
123
+ level: level,
124
+ service: config.service,
125
+ env: config.environment,
126
+ message: message,
127
+ event_type: "http.request",
128
+ method: req[:method],
129
+ path: req[:path],
130
+ status: status.to_i,
131
+ duration_ms: duration_ms.to_i,
132
+ controller: doc[:controller],
133
+ }
134
+
135
+ payload[:trace_id] = doc[:trace_id] if doc[:trace_id]
136
+ payload[:span_id] = doc[:span_id] if doc[:span_id]
137
+ payload[:parent_span_id] = doc[:parent_span_id] if doc[:parent_span_id]
138
+ payload[:request_id] = doc[:request_id] if doc[:request_id]
139
+
140
+ body = {}
141
+ body[:request_headers] = req[:headers] if req[:headers]
142
+ body[:request_params] = req[:params] if req[:params]
143
+ body[:request_body] = req[:body] if req[:body]
144
+ body[:response_headers] = resp[:headers] if resp[:headers]
145
+ body[:response_body] = resp[:body] if resp[:body]
146
+ body[:sql] = doc[:sql] if doc[:sql] && !doc[:sql].empty?
147
+ body[:http] = doc[:http] if doc[:http] && !doc[:http].empty?
148
+ body[:email] = doc[:email] if doc[:email] && !doc[:email].empty?
149
+ body[:audit] = doc[:audit] if doc[:audit] && !doc[:audit].empty?
150
+ body[:logs] = doc[:logs] if doc[:logs] && !doc[:logs].empty?
151
+ body[:timeline] = doc[:timeline] if doc[:timeline] && !doc[:timeline].empty?
152
+ body[:performance] = doc[:performance] if doc[:performance]
153
+ body[:context] = doc[:context] if doc[:context]
154
+
155
+ if doc[:pending_explains] && defined?(ActiveRecord::Base)
156
+ explain_results = run_pending_explains(doc[:pending_explains])
157
+ body[:queries] = explain_results unless explain_results.empty?
158
+ end
159
+
160
+ payload[:body] = body unless body.empty?
161
+ payload
162
+ rescue StandardError
163
+ nil
164
+ end
165
+
96
166
  def materialize_request(entry, config)
97
167
  _, started, finished, controller, action, method, path, status,
98
168
  exc_class, exc_message, exc_backtrace, request_id, trace_id,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module OpenTrace
4
6
  module PiiScrubber
5
7
  REDACTED = "[REDACTED]"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  if defined?(::Rails::Railtie)
4
6
  module OpenTrace
5
7
  class Railtie < ::Rails::Railtie
@@ -9,10 +9,10 @@ module OpenTrace
9
9
  # :standard — includes captures but strips request/response bodies
10
10
  # :full — everything
11
11
  CAPTURE_LEVELS = %i[minimal standard full].freeze
12
- CAPTURE_LEVEL_RANK = { minimal: 0, standard: 1, full: 2 }.freeze
12
+ CAPTURE_LEVEL_RANK = { none: -1, minimal: 0, standard: 1, full: 2 }.freeze
13
13
 
14
14
  # Domains that can have individual capture-level overrides
15
- DOMAINS = %i[sql http email file audit log timeline].freeze
15
+ DOMAINS = %i[request sql http email file audit log timeline].freeze
16
16
 
17
17
  BASE_BYTE_ESTIMATE = 1024 # 1KB base overhead
18
18
 
@@ -378,7 +378,7 @@ module OpenTrace
378
378
 
379
379
  def filter_captures(captures, domain, base_rank, domain_overrides)
380
380
  rank = resolve_domain_rank(domain, base_rank, domain_overrides)
381
- return nil if rank == 0 # :minimal — strip entirely
381
+ return nil if rank <= 0 # :none / :minimal — strip entirely
382
382
  return captures.map(&:dup) if rank >= 2 # :full — everything
383
383
 
384
384
  # :standard — strip body fields
@@ -410,6 +410,7 @@ module OpenTrace
410
410
  end
411
411
 
412
412
  def build_request_section(base_rank, domain_overrides)
413
+ rank = resolve_domain_rank(:request, base_rank, domain_overrides)
413
414
  section = {
414
415
  method: @request_method,
415
416
  path: @request_path,
@@ -420,13 +421,13 @@ module OpenTrace
420
421
  size: @request_size
421
422
  }.compact
422
423
 
423
- if base_rank >= 2 # :full
424
+ if rank >= 2 # :full
424
425
  section[:headers] = @request_headers if @request_headers
425
426
  section[:body] = @request_body if @request_body
426
427
  section[:params] = @request_params if @request_params
427
428
  section[:cookies] = @cookies if @cookies
428
429
  section[:session_data] = @session_data if @session_data
429
- elsif base_rank >= 1 # :standard — params but no body
430
+ elsif rank >= 1 # :standard — params but no body
430
431
  section[:params] = @request_params if @request_params
431
432
  end
432
433
 
@@ -434,12 +435,13 @@ module OpenTrace
434
435
  end
435
436
 
436
437
  def build_response_section(base_rank, domain_overrides)
438
+ rank = resolve_domain_rank(:request, base_rank, domain_overrides)
437
439
  section = {
438
440
  status: @response_status,
439
441
  size: @response_size
440
442
  }.compact
441
443
 
442
- if base_rank >= 2 # :full
444
+ if rank >= 2 # :full
443
445
  section[:headers] = @response_headers if @response_headers
444
446
  section[:body] = @response_body if @response_body
445
447
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenTrace
4
- VERSION = "0.17.1"
4
+ VERSION = "0.17.3"
5
5
  end
data/lib/opentrace.rb CHANGED
@@ -61,6 +61,7 @@ module OpenTrace
61
61
  def configure
62
62
  yield config
63
63
  config.finalize!
64
+ InstrumentationContext.configure!(config)
64
65
  reset_client!
65
66
  end
66
67
 
@@ -289,61 +290,12 @@ module OpenTrace
289
290
  end
290
291
 
291
292
  # Enqueue a raw request buffer document (from InstrumentationContext).
292
- # Converts the nested buffer doc to flat format before enqueueing.
293
+ # Defers conversion to the background dispatch thread.
293
294
  def client_enqueue_raw(doc)
294
295
  return unless enabled?
296
+ return unless doc.is_a?(Hash)
295
297
 
296
- req = doc[:request] || {}
297
- resp = doc[:response] || {}
298
- duration_ms = if doc[:started_at]
299
- ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - doc[:started_at]) * 1000).round(0)
300
- end
301
-
302
- status = resp[:status] || resp[:response_status]
303
- level = if status.to_i >= 500 then "error"
304
- elsif status.to_i >= 400 then "warn"
305
- else "info"
306
- end
307
-
308
- message = "#{req[:method]} #{req[:path]} #{status} #{duration_ms}ms"
309
-
310
- payload = {
311
- ts: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ"),
312
- level: level,
313
- service: config.service,
314
- env: config.environment,
315
- message: message,
316
- event_type: "http.request",
317
- method: req[:method],
318
- path: req[:path],
319
- status: status.to_i,
320
- duration_ms: duration_ms.to_i,
321
- controller: doc[:controller],
322
- }
323
-
324
- payload[:trace_id] = doc[:trace_id] if doc[:trace_id]
325
- payload[:span_id] = doc[:span_id] if doc[:span_id]
326
- payload[:parent_span_id] = doc[:parent_span_id] if doc[:parent_span_id]
327
- payload[:request_id] = doc[:request_id] if doc[:request_id]
328
-
329
- # Pack everything else into body
330
- body = {}
331
- body[:request_headers] = req[:headers] if req[:headers]
332
- body[:request_params] = req[:params] if req[:params]
333
- body[:request_body] = req[:body] if req[:body]
334
- body[:response_headers] = resp[:headers] if resp[:headers]
335
- body[:response_body] = resp[:body] if resp[:body]
336
- body[:sql] = doc[:sql] if doc[:sql] && !doc[:sql].empty?
337
- body[:http] = doc[:http] if doc[:http] && !doc[:http].empty?
338
- body[:email] = doc[:email] if doc[:email] && !doc[:email].empty?
339
- body[:audit] = doc[:audit] if doc[:audit] && !doc[:audit].empty?
340
- body[:logs] = doc[:logs] if doc[:logs] && !doc[:logs].empty?
341
- body[:timeline] = doc[:timeline] if doc[:timeline] && !doc[:timeline].empty?
342
- body[:performance] = doc[:performance] if doc[:performance]
343
- body[:context] = doc[:context] if doc[:context]
344
-
345
- payload[:body] = body unless body.empty?
346
- client.enqueue(payload)
298
+ client.enqueue([:raw_document, doc])
347
299
  rescue StandardError
348
300
  # Never raise to the host app
349
301
  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.17.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenTrace