opentrace 0.14.1 → 0.15.1
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/circuit_breaker.rb +16 -2
- data/lib/opentrace/client.rb +22 -6
- data/lib/opentrace/config.rb +10 -2
- data/lib/opentrace/middleware.rb +21 -11
- data/lib/opentrace/payload_builder.rb +50 -4
- data/lib/opentrace/stats.rb +1 -3
- data/lib/opentrace/version.rb +1 -1
- data/lib/opentrace.rb +5 -1
- 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: e5c67d0aecd2be7ec0624bd339bda99a46d8c2de8d0a04009d6778f94cefee3d
|
|
4
|
+
data.tar.gz: 0b54c97978becc24a2339e1f615de4f715388fc9247dc88099880dc7e1532a43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60bc99ef6e4e950851358a9dbf3b2b8342209f81ff24d58eb248c07362657d4c040c1f60e6e8fdb3cb56d625cac4f8b9a4641e953241a1af52195f27232790f6
|
|
7
|
+
data.tar.gz: d655b2237f513c8b3fe8a12e936086937fc80b1f50d5bb3766b6e5ef3ed1a137d90bf9798546aa99413633dba9e9588c87f2b99d4e54e226c0842fea720f309e
|
|
@@ -14,6 +14,7 @@ module OpenTrace
|
|
|
14
14
|
@state = CLOSED
|
|
15
15
|
@failure_count = 0
|
|
16
16
|
@last_failure_at = nil
|
|
17
|
+
@half_open_probe_sent = false
|
|
17
18
|
@mutex = Mutex.new
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -25,12 +26,18 @@ module OpenTrace
|
|
|
25
26
|
when OPEN
|
|
26
27
|
if Time.now - @last_failure_at >= @recovery_timeout
|
|
27
28
|
@state = HALF_OPEN
|
|
29
|
+
@half_open_probe_sent = true
|
|
28
30
|
true
|
|
29
31
|
else
|
|
30
32
|
false
|
|
31
33
|
end
|
|
32
34
|
when HALF_OPEN
|
|
33
|
-
|
|
35
|
+
if @half_open_probe_sent
|
|
36
|
+
false
|
|
37
|
+
else
|
|
38
|
+
@half_open_probe_sent = true
|
|
39
|
+
true
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
end
|
|
@@ -38,6 +45,7 @@ module OpenTrace
|
|
|
38
45
|
def record_success
|
|
39
46
|
@mutex.synchronize do
|
|
40
47
|
@failure_count = 0
|
|
48
|
+
@half_open_probe_sent = false
|
|
41
49
|
@state = CLOSED
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -46,7 +54,12 @@ module OpenTrace
|
|
|
46
54
|
@mutex.synchronize do
|
|
47
55
|
@failure_count += 1
|
|
48
56
|
@last_failure_at = Time.now
|
|
49
|
-
|
|
57
|
+
if @state == HALF_OPEN
|
|
58
|
+
@half_open_probe_sent = false
|
|
59
|
+
@state = OPEN
|
|
60
|
+
elsif @failure_count >= @failure_threshold
|
|
61
|
+
@state = OPEN
|
|
62
|
+
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
65
|
|
|
@@ -55,6 +68,7 @@ module OpenTrace
|
|
|
55
68
|
@state = CLOSED
|
|
56
69
|
@failure_count = 0
|
|
57
70
|
@last_failure_at = nil
|
|
71
|
+
@half_open_probe_sent = false
|
|
58
72
|
end
|
|
59
73
|
end
|
|
60
74
|
end
|
data/lib/opentrace/client.rb
CHANGED
|
@@ -322,11 +322,20 @@ module OpenTrace
|
|
|
322
322
|
@rate_limit_until = Time.now + retry_after
|
|
323
323
|
|
|
324
324
|
# Re-enqueue batch items if space allows
|
|
325
|
+
re_enqueued = 0
|
|
325
326
|
batch.each do |payload|
|
|
326
327
|
break if @queue.size >= MAX_QUEUE_SIZE
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
begin
|
|
329
|
+
@queue.push(payload)
|
|
330
|
+
re_enqueued += 1
|
|
331
|
+
rescue ClosedQueueError
|
|
332
|
+
break
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
dropped = batch.size - re_enqueued
|
|
336
|
+
if dropped > 0
|
|
337
|
+
@stats.increment(:dropped_error, dropped)
|
|
338
|
+
fire_on_drop(dropped, :shutdown)
|
|
330
339
|
end
|
|
331
340
|
end
|
|
332
341
|
|
|
@@ -404,6 +413,8 @@ module OpenTrace
|
|
|
404
413
|
end
|
|
405
414
|
|
|
406
415
|
def unix_socket_send(json)
|
|
416
|
+
require "socket" unless defined?(UNIXSocket)
|
|
417
|
+
|
|
407
418
|
payload = if @config.compression && json.bytesize > @config.compression_threshold
|
|
408
419
|
gzip_compress(json)
|
|
409
420
|
else
|
|
@@ -416,9 +427,14 @@ module OpenTrace
|
|
|
416
427
|
socket.write(payload)
|
|
417
428
|
socket.flush
|
|
418
429
|
|
|
419
|
-
# Read 4-byte status code response
|
|
420
|
-
|
|
421
|
-
|
|
430
|
+
# Read 4-byte status code response with timeout
|
|
431
|
+
if IO.select([socket], nil, nil, 5)
|
|
432
|
+
response_data = socket.read(4)
|
|
433
|
+
status = response_data&.unpack1("N") || 500
|
|
434
|
+
else
|
|
435
|
+
@stats.increment(:socket_timeouts)
|
|
436
|
+
status = 500
|
|
437
|
+
end
|
|
422
438
|
socket.close
|
|
423
439
|
|
|
424
440
|
UnixSocketResponse.new(status)
|
data/lib/opentrace/config.rb
CHANGED
|
@@ -23,7 +23,7 @@ module OpenTrace
|
|
|
23
23
|
:trace_propagation,
|
|
24
24
|
:log_forwarding, :view_tracking, :cache_tracking,
|
|
25
25
|
:deprecation_tracking, :detailed_request_log,
|
|
26
|
-
:
|
|
26
|
+
:sampler, :before_send,
|
|
27
27
|
:sql_normalization, :log_trace_injection,
|
|
28
28
|
:source_context, :before_breadcrumb,
|
|
29
29
|
:pii_scrubbing, :pii_patterns, :pii_disabled_patterns,
|
|
@@ -35,7 +35,7 @@ module OpenTrace
|
|
|
35
35
|
:runtime_metrics, :runtime_metrics_interval
|
|
36
36
|
|
|
37
37
|
# Custom writers that invalidate caches
|
|
38
|
-
attr_reader :enabled, :min_level, :allowed_levels, :ignore_paths
|
|
38
|
+
attr_reader :enabled, :min_level, :allowed_levels, :ignore_paths, :sample_rate
|
|
39
39
|
|
|
40
40
|
def enabled=(val)
|
|
41
41
|
@enabled = val
|
|
@@ -56,6 +56,14 @@ module OpenTrace
|
|
|
56
56
|
@ignore_paths = val
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def sample_rate=(val)
|
|
60
|
+
val = val.to_f
|
|
61
|
+
unless (0.0..1.0).cover?(val)
|
|
62
|
+
raise ArgumentError, "sample_rate must be between 0.0 and 1.0, got #{val}"
|
|
63
|
+
end
|
|
64
|
+
@sample_rate = val
|
|
65
|
+
end
|
|
66
|
+
|
|
59
67
|
def initialize
|
|
60
68
|
@endpoint = nil
|
|
61
69
|
@api_key = nil
|
data/lib/opentrace/middleware.rb
CHANGED
|
@@ -5,6 +5,22 @@ require_relative "trace_context"
|
|
|
5
5
|
|
|
6
6
|
module OpenTrace
|
|
7
7
|
class Middleware
|
|
8
|
+
# All Fiber-local keys managed by this middleware.
|
|
9
|
+
# Centralised so cleanup never misses one.
|
|
10
|
+
FIBER_KEYS = %i[
|
|
11
|
+
opentrace_collector
|
|
12
|
+
opentrace_cached_context
|
|
13
|
+
opentrace_sql_count
|
|
14
|
+
opentrace_sql_total_ms
|
|
15
|
+
opentrace_trace_id
|
|
16
|
+
opentrace_span_id
|
|
17
|
+
opentrace_parent_span_id
|
|
18
|
+
opentrace_transaction_name
|
|
19
|
+
opentrace_breadcrumbs
|
|
20
|
+
opentrace_session_id
|
|
21
|
+
opentrace_pending_explains
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
8
24
|
def initialize(app)
|
|
9
25
|
@app = app
|
|
10
26
|
end
|
|
@@ -65,22 +81,16 @@ module OpenTrace
|
|
|
65
81
|
collector.memory_after = current_rss_mb
|
|
66
82
|
end
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
Fiber[:opentrace_cached_context] = nil
|
|
70
|
-
Fiber[:opentrace_sql_count] = nil
|
|
71
|
-
Fiber[:opentrace_sql_total_ms] = nil
|
|
72
|
-
Fiber[:opentrace_trace_id] = nil
|
|
73
|
-
Fiber[:opentrace_span_id] = nil
|
|
74
|
-
Fiber[:opentrace_parent_span_id] = nil
|
|
75
|
-
Fiber[:opentrace_transaction_name] = nil
|
|
76
|
-
Fiber[:opentrace_breadcrumbs] = nil
|
|
77
|
-
Fiber[:opentrace_session_id] = nil
|
|
78
|
-
Fiber[:opentrace_pending_explains] = nil
|
|
84
|
+
cleanup_fiber_locals
|
|
79
85
|
OpenTrace.current_request_id = nil
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
private
|
|
83
89
|
|
|
90
|
+
def cleanup_fiber_locals
|
|
91
|
+
FIBER_KEYS.each { |key| Fiber[key] = nil }
|
|
92
|
+
end
|
|
93
|
+
|
|
84
94
|
# Extract trace context from incoming request headers.
|
|
85
95
|
# Priority: W3C traceparent > X-Trace-ID > request_id > generate new
|
|
86
96
|
def extract_trace_context(env)
|
|
@@ -13,7 +13,9 @@ module OpenTrace
|
|
|
13
13
|
elsif entry.is_a?(Hash)
|
|
14
14
|
entry # legacy direct payload
|
|
15
15
|
end
|
|
16
|
-
rescue StandardError
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
OpenTrace.stats.increment(:payload_build_errors) if OpenTrace.respond_to?(:stats)
|
|
18
|
+
$stderr.puts "[OpenTrace] PayloadBuilder error: #{e.class}: #{e.message}" if OpenTrace.respond_to?(:config) && OpenTrace.config.respond_to?(:debug) && OpenTrace.config.debug
|
|
17
19
|
nil
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -26,12 +28,18 @@ module OpenTrace
|
|
|
26
28
|
|
|
27
29
|
static_ctx = OpenTrace.send(:static_context)
|
|
28
30
|
static_ctx.each { |k, v| meta[k] ||= v }
|
|
29
|
-
meta[:request_id] ||= request_id if request_id
|
|
30
31
|
|
|
31
32
|
# Extract trace_id from metadata if user provided it there
|
|
32
33
|
meta_trace_id = meta.delete(:trace_id)
|
|
33
34
|
effective_trace_id = meta_trace_id || trace_id
|
|
34
35
|
|
|
36
|
+
# Promote indexed fields to top-level (remove from metadata to avoid duplication)
|
|
37
|
+
commit_hash = meta.delete(:git_sha)
|
|
38
|
+
effective_request_id = meta.delete(:request_id) || request_id
|
|
39
|
+
exception_class = meta.delete(:exception_class)
|
|
40
|
+
error_fingerprint = meta.delete(:error_fingerprint)
|
|
41
|
+
source_file, source_line = extract_source_location(meta[:backtrace])
|
|
42
|
+
|
|
35
43
|
payload = {
|
|
36
44
|
timestamp: format_timestamp(ts),
|
|
37
45
|
level: level.to_s.upcase,
|
|
@@ -41,6 +49,12 @@ module OpenTrace
|
|
|
41
49
|
metadata: meta.compact
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
payload[:commit_hash] = commit_hash if commit_hash
|
|
53
|
+
payload[:request_id] = effective_request_id.to_s if effective_request_id
|
|
54
|
+
payload[:exception_class] = exception_class if exception_class
|
|
55
|
+
payload[:error_fingerprint] = error_fingerprint if error_fingerprint
|
|
56
|
+
payload[:source_file] = source_file if source_file
|
|
57
|
+
payload[:source_line] = source_line if source_line && source_line > 0
|
|
44
58
|
payload[:event_type] = event_type.to_s if event_type
|
|
45
59
|
payload[:trace_id] = effective_trace_id.to_s if effective_trace_id
|
|
46
60
|
payload[:span_id] = span_id if span_id
|
|
@@ -67,13 +81,19 @@ module OpenTrace
|
|
|
67
81
|
meta[:user_id] = cached_ctx[:user_id]
|
|
68
82
|
end
|
|
69
83
|
|
|
84
|
+
exception_class = nil
|
|
85
|
+
error_fingerprint = nil
|
|
86
|
+
source_file = nil
|
|
87
|
+
source_line = nil
|
|
88
|
+
|
|
70
89
|
if exc_class
|
|
71
|
-
|
|
90
|
+
exception_class = exc_class
|
|
72
91
|
meta[:exception_message] = exc_message&.slice(0, 500)
|
|
73
92
|
if exc_backtrace
|
|
74
93
|
cleaned = clean_backtrace(exc_backtrace)
|
|
75
94
|
meta[:backtrace] = cleaned.first(15)
|
|
76
|
-
|
|
95
|
+
error_fingerprint = OpenTrace.send(:compute_error_fingerprint, exc_class, cleaned)
|
|
96
|
+
source_file, source_line = extract_source_location(cleaned)
|
|
77
97
|
end
|
|
78
98
|
end
|
|
79
99
|
|
|
@@ -117,6 +137,10 @@ module OpenTrace
|
|
|
117
137
|
end
|
|
118
138
|
meta[:transaction_name] = transaction_name if transaction_name
|
|
119
139
|
|
|
140
|
+
# Promote indexed fields to top-level (remove from metadata to avoid duplication)
|
|
141
|
+
commit_hash = meta.delete(:git_sha)
|
|
142
|
+
effective_request_id = meta.delete(:request_id) || request_id
|
|
143
|
+
|
|
120
144
|
payload = {
|
|
121
145
|
timestamp: format_timestamp(started),
|
|
122
146
|
level: level,
|
|
@@ -125,6 +149,12 @@ module OpenTrace
|
|
|
125
149
|
message: message,
|
|
126
150
|
metadata: meta.compact
|
|
127
151
|
}
|
|
152
|
+
payload[:commit_hash] = commit_hash if commit_hash
|
|
153
|
+
payload[:request_id] = effective_request_id.to_s if effective_request_id
|
|
154
|
+
payload[:exception_class] = exception_class if exception_class
|
|
155
|
+
payload[:error_fingerprint] = error_fingerprint if error_fingerprint
|
|
156
|
+
payload[:source_file] = source_file if source_file
|
|
157
|
+
payload[:source_line] = source_line if source_line && source_line > 0
|
|
128
158
|
payload[:trace_id] = trace_id.to_s if trace_id
|
|
129
159
|
payload[:span_id] = span_id if span_id
|
|
130
160
|
payload[:parent_span_id] = parent_span_id if parent_span_id
|
|
@@ -143,6 +173,22 @@ module OpenTrace
|
|
|
143
173
|
end
|
|
144
174
|
end
|
|
145
175
|
|
|
176
|
+
# Extract source file and line number from the first app-relevant backtrace line.
|
|
177
|
+
# Format: "app/controllers/users_controller.rb:42:in `show'"
|
|
178
|
+
def extract_source_location(backtrace)
|
|
179
|
+
return [nil, nil] unless backtrace.is_a?(Array) && !backtrace.empty?
|
|
180
|
+
|
|
181
|
+
line = backtrace.first.to_s
|
|
182
|
+
parts = line.split(":", 3)
|
|
183
|
+
return [nil, nil] if parts.length < 2
|
|
184
|
+
|
|
185
|
+
file = parts[0]
|
|
186
|
+
line_num = parts[1].to_i
|
|
187
|
+
[file, line_num]
|
|
188
|
+
rescue StandardError
|
|
189
|
+
[nil, nil]
|
|
190
|
+
end
|
|
191
|
+
|
|
146
192
|
def clean_backtrace(backtrace)
|
|
147
193
|
if defined?(::Rails) && ::Rails.respond_to?(:backtrace_cleaner)
|
|
148
194
|
::Rails.backtrace_cleaner.clean(backtrace)
|
data/lib/opentrace/stats.rb
CHANGED
|
@@ -26,10 +26,8 @@ module OpenTrace
|
|
|
26
26
|
@started_at = Time.now
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# Hot path: no mutex. Under CRuby's GIL, Hash#[]= with integer
|
|
30
|
-
# increment is effectively atomic for single operations.
|
|
31
29
|
def increment(counter, amount = 1)
|
|
32
|
-
@counters[counter] += amount
|
|
30
|
+
@mutex.synchronize { @counters[counter] += amount }
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
def get(counter)
|
data/lib/opentrace/version.rb
CHANGED
data/lib/opentrace.rb
CHANGED
|
@@ -25,13 +25,17 @@ module OpenTrace
|
|
|
25
25
|
# Null object for when OpenTrace is not configured.
|
|
26
26
|
# All methods are no-ops, avoiding nil checks on the hot path.
|
|
27
27
|
class NilClient
|
|
28
|
+
def initialize
|
|
29
|
+
@nil_stats = NilStats.new
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def enqueue(_) = nil
|
|
29
33
|
def shutdown(timeout: 5) = nil
|
|
30
34
|
def queue_size = 0
|
|
31
35
|
def circuit_state = :closed
|
|
32
36
|
def auth_suspended? = false
|
|
33
37
|
def stats_snapshot = { queue_size: 0, circuit_state: :closed, auth_suspended: false }
|
|
34
|
-
def stats =
|
|
38
|
+
def stats = @nil_stats
|
|
35
39
|
def supports?(_) = false
|
|
36
40
|
end
|
|
37
41
|
|