opentrace 0.17.0 → 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 +4 -4
- data/lib/opentrace/client.rb +61 -9
- data/lib/opentrace/http_tracker.rb +49 -24
- data/lib/opentrace/instrumentation_context.rb +80 -28
- data/lib/opentrace/logger.rb +25 -4
- data/lib/opentrace/middleware.rb +40 -19
- data/lib/opentrace/payload_builder.rb +71 -1
- data/lib/opentrace/pii_scrubber.rb +2 -0
- data/lib/opentrace/rails.rb +2 -0
- data/lib/opentrace/request_buffer.rb +8 -6
- data/lib/opentrace/version.rb +1 -1
- data/lib/opentrace.rb +4 -52
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2ea4cd5b52e24bbbed81f9d9c9f522f8001bf379407cd01c126b659d0a4efe5
|
|
4
|
+
data.tar.gz: 52cb261f68c4e1280b1c164fb732e9a02123abd70a0274e70ed8894af76b9c9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b5cbf4a21b4e09f9246cfa89e8d45f21f6b7b6ad4d232a2b909a8b76fd9ce6829cee208b8bfec596198954de50574a52d249dc63761c8be3e394640f3885b9d
|
|
7
|
+
data.tar.gz: d7714397187d069769743c30d39a3ccf865624c40ba86d6f3c9a0fad5bb56f10417660045eb4c07123dd0260359b104f39fd2449beb7371f32512836cd585257
|
data/lib/opentrace/client.rb
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
@@ -43,14 +43,34 @@ module OpenTrace
|
|
|
43
43
|
# Guard 2: skip if this IS an OpenTrace dispatch call (prevent infinite recursion)
|
|
44
44
|
return super if Fiber[:opentrace_http_tracking_disabled]
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
46
|
+
# Trace context injection is bookkeeping — must never raise to the host.
|
|
47
|
+
begin
|
|
48
|
+
inject_trace_context(req) if OpenTrace.config.trace_propagation
|
|
49
|
+
rescue StandardError
|
|
50
|
+
end
|
|
48
51
|
|
|
49
|
-
collector = Fiber[:opentrace_collector]
|
|
50
52
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
begin
|
|
55
|
+
response = super
|
|
56
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError, Timeout::Error, Net::ProtocolError => e
|
|
57
|
+
# Real network error from the host's HTTP call. Record it, then re-raise
|
|
58
|
+
# so the host app sees the error it's expecting to handle.
|
|
59
|
+
record_http_failure(req, e, start_time) rescue nil
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Bookkeeping for a successful response. Any bug here (NoMethodError on
|
|
64
|
+
# a streaming body, a payload builder regression, etc.) must NOT bubble
|
|
65
|
+
# into host code — swallow and move on.
|
|
66
|
+
record_http_success(req, response, start_time) rescue nil
|
|
67
|
+
|
|
68
|
+
response
|
|
69
|
+
end
|
|
53
70
|
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def record_http_success(req, response, start_time)
|
|
54
74
|
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000
|
|
55
75
|
host = address
|
|
56
76
|
port_str = (port == 443 || port == 80) ? "" : ":#{port}"
|
|
@@ -58,18 +78,11 @@ module OpenTrace
|
|
|
58
78
|
safe_path = req.path.to_s.split("?").first
|
|
59
79
|
url = "#{scheme}://#{host}#{port_str}#{safe_path}"
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
req_body = req.body
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Capture response body (if under size limit)
|
|
68
|
-
resp_body = nil
|
|
69
|
-
if response.body && response.body.is_a?(String) && response.body.bytesize < MAX_BODY_CAPTURE_BYTES
|
|
70
|
-
resp_body = response.body
|
|
71
|
-
end
|
|
81
|
+
req_body = capturable_body(req.body)
|
|
82
|
+
resp_body = capturable_body(response.body)
|
|
83
|
+
resp_size = body_size(response)
|
|
72
84
|
|
|
85
|
+
collector = Fiber[:opentrace_collector]
|
|
73
86
|
if collector
|
|
74
87
|
collector.record_http(
|
|
75
88
|
method: req.method,
|
|
@@ -90,22 +103,23 @@ module OpenTrace
|
|
|
90
103
|
vendor: vendor,
|
|
91
104
|
status: response.code.to_i,
|
|
92
105
|
duration_ms: duration_ms,
|
|
93
|
-
request_headers: nil,
|
|
106
|
+
request_headers: nil,
|
|
94
107
|
request_body: req_body,
|
|
95
108
|
response_headers: nil,
|
|
96
109
|
response_body: resp_body,
|
|
97
|
-
response_size:
|
|
110
|
+
response_size: resp_size,
|
|
98
111
|
retry_attempt: 0,
|
|
99
112
|
error_class: nil
|
|
100
113
|
)
|
|
101
114
|
buffer.record_timeline(type: :http, name: "#{req.method} #{host}", duration_ms: duration_ms)
|
|
102
115
|
end
|
|
116
|
+
end
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError, Timeout::Error, Net::ProtocolError => e
|
|
106
|
-
# Record the failed HTTP call, then re-raise
|
|
118
|
+
def record_http_failure(req, error, start_time)
|
|
107
119
|
duration_ms = start_time ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000 : 0
|
|
120
|
+
req_body = req ? capturable_body(req.body) : nil
|
|
108
121
|
|
|
122
|
+
collector = Fiber[:opentrace_collector]
|
|
109
123
|
if collector
|
|
110
124
|
collector.record_http(
|
|
111
125
|
method: req&.method,
|
|
@@ -113,7 +127,7 @@ module OpenTrace
|
|
|
113
127
|
host: address,
|
|
114
128
|
status: 0,
|
|
115
129
|
duration_ms: duration_ms,
|
|
116
|
-
error:
|
|
130
|
+
error: error.class.name
|
|
117
131
|
)
|
|
118
132
|
end
|
|
119
133
|
|
|
@@ -130,14 +144,25 @@ module OpenTrace
|
|
|
130
144
|
request_body: req_body,
|
|
131
145
|
response_body: nil,
|
|
132
146
|
response_size: nil,
|
|
133
|
-
error_class:
|
|
147
|
+
error_class: error.class.name
|
|
134
148
|
)
|
|
135
149
|
end
|
|
150
|
+
end
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
def capturable_body(body)
|
|
153
|
+
return nil unless body.is_a?(String)
|
|
154
|
+
return nil if body.bytesize >= MAX_BODY_CAPTURE_BYTES
|
|
155
|
+
body
|
|
138
156
|
end
|
|
139
157
|
|
|
140
|
-
|
|
158
|
+
# Response body size when it can be determined without re-reading the stream.
|
|
159
|
+
# Streaming responses (Net::ReadAdapter) can't be re-read, so we fall back
|
|
160
|
+
# to Content-Length when present, otherwise nil.
|
|
161
|
+
def body_size(response)
|
|
162
|
+
return response.body.bytesize if response.body.is_a?(String)
|
|
163
|
+
len = response["Content-Length"]
|
|
164
|
+
len ? len.to_i : nil
|
|
165
|
+
end
|
|
141
166
|
|
|
142
167
|
def inject_trace_context(req)
|
|
143
168
|
trace_id = Fiber[:opentrace_trace_id]
|
|
@@ -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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
data/lib/opentrace/logger.rb
CHANGED
|
@@ -24,11 +24,32 @@ module OpenTrace
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def add(severity, message = nil, progname = nil, &block)
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
data/lib/opentrace/middleware.rb
CHANGED
|
@@ -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
|
|
176
|
-
#
|
|
177
|
-
#
|
|
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
|
-
|
|
183
|
-
|
|
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]
|
|
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,
|
data/lib/opentrace/rails.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
data/lib/opentrace/version.rb
CHANGED
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
|
-
#
|
|
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
|
-
|
|
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
|