sentry-ruby 0.1.1 → 4.0.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 +4 -4
- data/.craft.yml +1 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +5 -0
- data/README.md +197 -21
- data/Rakefile +3 -1
- data/lib/{sentry.rb → sentry-ruby.rb} +29 -3
- data/lib/sentry/benchmarks/benchmark_transport.rb +14 -0
- data/lib/sentry/breadcrumb.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +10 -26
- data/lib/sentry/breadcrumb_buffer.rb +2 -5
- data/lib/sentry/client.rb +32 -37
- data/lib/sentry/configuration.rb +77 -91
- data/lib/sentry/dsn.rb +6 -3
- data/lib/sentry/event.rb +42 -40
- data/lib/sentry/hub.rb +13 -2
- data/lib/sentry/interfaces/request.rb +1 -10
- data/lib/sentry/rack.rb +1 -0
- data/lib/sentry/rack/tracing.rb +39 -0
- data/lib/sentry/scope.rb +26 -4
- data/lib/sentry/span.rb +155 -0
- data/lib/sentry/transaction.rb +113 -0
- data/lib/sentry/transaction_event.rb +29 -0
- data/lib/sentry/transport.rb +11 -24
- data/lib/sentry/transport/configuration.rb +1 -8
- data/lib/sentry/transport/http_transport.rb +5 -2
- data/lib/sentry/transport/state.rb +2 -2
- data/lib/sentry/utils/request_id.rb +16 -0
- data/lib/sentry/version.rb +1 -1
- metadata +9 -6
- data/lib/sentry/event/options.rb +0 -31
- data/lib/sentry/ruby.rb +0 -1
- data/lib/sentry/utils/deep_merge.rb +0 -22
data/lib/sentry/hub.rb
CHANGED
@@ -67,6 +67,12 @@ module Sentry
|
|
67
67
|
@stack.pop
|
68
68
|
end
|
69
69
|
|
70
|
+
def start_transaction(transaction: nil, **options)
|
71
|
+
transaction ||= Transaction.new(**options)
|
72
|
+
transaction.set_initial_sample_desicion
|
73
|
+
transaction
|
74
|
+
end
|
75
|
+
|
70
76
|
def capture_exception(exception, **options, &block)
|
71
77
|
return unless current_client
|
72
78
|
|
@@ -74,12 +80,16 @@ module Sentry
|
|
74
80
|
|
75
81
|
return unless event
|
76
82
|
|
83
|
+
options[:hint] ||= {}
|
84
|
+
options[:hint] = options[:hint].merge(exception: exception)
|
77
85
|
capture_event(event, **options, &block)
|
78
86
|
end
|
79
87
|
|
80
88
|
def capture_message(message, **options, &block)
|
81
89
|
return unless current_client
|
82
90
|
|
91
|
+
options[:hint] ||= {}
|
92
|
+
options[:hint] = options[:hint].merge(message: message)
|
83
93
|
event = current_client.event_from_message(message)
|
84
94
|
capture_event(event, **options, &block)
|
85
95
|
end
|
@@ -87,6 +97,7 @@ module Sentry
|
|
87
97
|
def capture_event(event, **options, &block)
|
88
98
|
return unless current_client
|
89
99
|
|
100
|
+
hint = options.delete(:hint)
|
90
101
|
scope = current_scope.dup
|
91
102
|
|
92
103
|
if block
|
@@ -97,9 +108,9 @@ module Sentry
|
|
97
108
|
scope.update_from_options(**options)
|
98
109
|
end
|
99
110
|
|
100
|
-
event = current_client.capture_event(event, scope)
|
111
|
+
event = current_client.capture_event(event, scope, hint)
|
101
112
|
|
102
|
-
@last_event_id = event.
|
113
|
+
@last_event_id = event.event_id
|
103
114
|
event
|
104
115
|
end
|
105
116
|
|
@@ -39,15 +39,6 @@ module Sentry
|
|
39
39
|
|
40
40
|
private
|
41
41
|
|
42
|
-
# Request ID based on ActionDispatch::RequestId
|
43
|
-
def read_request_id_from(env_hash)
|
44
|
-
REQUEST_ID_HEADERS.each do |key|
|
45
|
-
request_id = env_hash[key]
|
46
|
-
return request_id if request_id
|
47
|
-
end
|
48
|
-
nil
|
49
|
-
end
|
50
|
-
|
51
42
|
# See Sentry server default limits at
|
52
43
|
# https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
|
53
44
|
def read_data_from(request)
|
@@ -67,7 +58,7 @@ module Sentry
|
|
67
58
|
begin
|
68
59
|
key = key.to_s # rack env can contain symbols
|
69
60
|
value = value.to_s
|
70
|
-
next memo['X-Request-Id'] ||=
|
61
|
+
next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env_hash) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
|
71
62
|
next unless key.upcase == key # Non-upper case stuff isn't either
|
72
63
|
|
73
64
|
# Rack adds in an incorrect HTTP_VERSION key, which causes downstream
|
data/lib/sentry/rack.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Sentry
|
2
|
+
module Rack
|
3
|
+
class Tracing
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
Sentry.clone_hub_to_current_thread unless Sentry.get_current_hub
|
10
|
+
|
11
|
+
if Sentry.configuration.traces_sample_rate.to_f == 0.0
|
12
|
+
return @app.call(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
Sentry.with_scope do |scope|
|
16
|
+
scope.clear_breadcrumbs
|
17
|
+
scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
|
18
|
+
span = Sentry.start_transaction(name: scope.transaction_name, op: "rack.request")
|
19
|
+
scope.set_span(span)
|
20
|
+
|
21
|
+
begin
|
22
|
+
response = @app.call(env)
|
23
|
+
rescue
|
24
|
+
finish_span(span, 500)
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
finish_span(span, response[0])
|
29
|
+
response
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def finish_span(span, status_code)
|
34
|
+
span.set_http_status(status_code)
|
35
|
+
span.finish
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/sentry/scope.rb
CHANGED
@@ -3,7 +3,7 @@ require "etc"
|
|
3
3
|
|
4
4
|
module Sentry
|
5
5
|
class Scope
|
6
|
-
ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env]
|
6
|
+
ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span]
|
7
7
|
|
8
8
|
attr_reader(*ATTRIBUTES)
|
9
9
|
|
@@ -15,20 +15,25 @@ module Sentry
|
|
15
15
|
set_default_value
|
16
16
|
end
|
17
17
|
|
18
|
-
def apply_to_event(event)
|
18
|
+
def apply_to_event(event, hint = nil)
|
19
19
|
event.tags = tags.merge(event.tags)
|
20
20
|
event.user = user.merge(event.user)
|
21
21
|
event.extra = extra.merge(event.extra)
|
22
22
|
event.contexts = contexts.merge(event.contexts)
|
23
|
+
|
24
|
+
if span
|
25
|
+
event.contexts[:trace] = span.get_trace_context
|
26
|
+
end
|
27
|
+
|
23
28
|
event.fingerprint = fingerprint
|
24
|
-
event.level
|
29
|
+
event.level = level
|
25
30
|
event.transaction = transaction_names.last
|
26
31
|
event.breadcrumbs = breadcrumbs
|
27
32
|
event.rack_env = rack_env
|
28
33
|
|
29
34
|
unless @event_processors.empty?
|
30
35
|
@event_processors.each do |processor_block|
|
31
|
-
event = processor_block.call(event)
|
36
|
+
event = processor_block.call(event, hint)
|
32
37
|
end
|
33
38
|
end
|
34
39
|
|
@@ -52,6 +57,7 @@ module Sentry
|
|
52
57
|
copy.user = user.deep_dup
|
53
58
|
copy.transaction_names = transaction_names.deep_dup
|
54
59
|
copy.fingerprint = fingerprint.deep_dup
|
60
|
+
copy.span = span
|
55
61
|
copy
|
56
62
|
end
|
57
63
|
|
@@ -63,6 +69,7 @@ module Sentry
|
|
63
69
|
self.user = scope.user
|
64
70
|
self.transaction_names = scope.transaction_names
|
65
71
|
self.fingerprint = scope.fingerprint
|
72
|
+
self.span = scope.span
|
66
73
|
end
|
67
74
|
|
68
75
|
def update_from_options(
|
@@ -86,6 +93,11 @@ module Sentry
|
|
86
93
|
@rack_env = env
|
87
94
|
end
|
88
95
|
|
96
|
+
def set_span(span)
|
97
|
+
check_argument_type!(span, Span)
|
98
|
+
@span = span
|
99
|
+
end
|
100
|
+
|
89
101
|
def set_user(user_hash)
|
90
102
|
check_argument_type!(user_hash, Hash)
|
91
103
|
@user = user_hash
|
@@ -130,6 +142,15 @@ module Sentry
|
|
130
142
|
@transaction_names.last
|
131
143
|
end
|
132
144
|
|
145
|
+
def get_transaction
|
146
|
+
# transaction will always be the first in the span_recorder
|
147
|
+
span.span_recorder.spans.first if span
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_span
|
151
|
+
span
|
152
|
+
end
|
153
|
+
|
133
154
|
def set_fingerprint(fingerprint)
|
134
155
|
check_argument_type!(fingerprint, Array)
|
135
156
|
|
@@ -164,6 +185,7 @@ module Sentry
|
|
164
185
|
@transaction_names = []
|
165
186
|
@event_processors = []
|
166
187
|
@rack_env = {}
|
188
|
+
@span = nil
|
167
189
|
end
|
168
190
|
|
169
191
|
class << self
|
data/lib/sentry/span.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module Sentry
|
5
|
+
class Span
|
6
|
+
STATUS_MAP = {
|
7
|
+
400 => "invalid_argument",
|
8
|
+
401 => "unauthenticated",
|
9
|
+
403 => "permission_denied",
|
10
|
+
404 => "not_found",
|
11
|
+
409 => "already_exists",
|
12
|
+
429 => "resource_exhausted",
|
13
|
+
499 => "cancelled",
|
14
|
+
500 => "internal_error",
|
15
|
+
501 => "unimplemented",
|
16
|
+
503 => "unavailable",
|
17
|
+
504 => "deadline_exceeded"
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
attr_reader :trace_id, :span_id, :parent_span_id, :sampled, :start_timestamp, :timestamp, :description, :op, :status, :tags, :data
|
22
|
+
attr_accessor :span_recorder
|
23
|
+
|
24
|
+
def initialize(description: nil, op: nil, status: nil, trace_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil)
|
25
|
+
@trace_id = trace_id || SecureRandom.uuid.delete("-")
|
26
|
+
@span_id = SecureRandom.hex(8)
|
27
|
+
@parent_span_id = parent_span_id
|
28
|
+
@sampled = sampled
|
29
|
+
@start_timestamp = start_timestamp || Sentry.utc_now.to_f
|
30
|
+
@timestamp = timestamp
|
31
|
+
@description = description
|
32
|
+
@op = op
|
33
|
+
@status = status
|
34
|
+
@data = {}
|
35
|
+
@tags = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_span_recorder
|
39
|
+
@span_recorder = SpanRecorder.new(1000)
|
40
|
+
@span_recorder.add(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def finish
|
44
|
+
# already finished
|
45
|
+
return if @timestamp
|
46
|
+
|
47
|
+
@timestamp = Sentry.utc_now.to_f
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_sentry_trace
|
52
|
+
sampled_flag = ""
|
53
|
+
sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
|
54
|
+
|
55
|
+
"#{@trace_id}-#{@span_id}-#{sampled_flag}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_hash
|
59
|
+
{
|
60
|
+
trace_id: @trace_id,
|
61
|
+
span_id: @span_id,
|
62
|
+
parent_span_id: @parent_span_id,
|
63
|
+
start_timestamp: @start_timestamp,
|
64
|
+
timestamp: @timestamp,
|
65
|
+
description: @description,
|
66
|
+
op: @op,
|
67
|
+
status: @status,
|
68
|
+
tags: @tags,
|
69
|
+
data: @data
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_trace_context
|
74
|
+
{
|
75
|
+
trace_id: @trace_id,
|
76
|
+
span_id: @span_id,
|
77
|
+
description: @description,
|
78
|
+
op: @op,
|
79
|
+
status: @status
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def start_child(**options)
|
84
|
+
options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
|
85
|
+
child_span = Span.new(options)
|
86
|
+
child_span.span_recorder = @span_recorder
|
87
|
+
|
88
|
+
if @span_recorder && @sampled
|
89
|
+
@span_recorder.add(child_span)
|
90
|
+
end
|
91
|
+
|
92
|
+
child_span
|
93
|
+
end
|
94
|
+
|
95
|
+
def with_child_span(**options, &block)
|
96
|
+
child_span = start_child(**options)
|
97
|
+
|
98
|
+
yield(child_span)
|
99
|
+
|
100
|
+
child_span.finish
|
101
|
+
end
|
102
|
+
|
103
|
+
def set_op(op)
|
104
|
+
@op = op
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_description(description)
|
108
|
+
@description = description
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_status(status)
|
112
|
+
@status = status
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_timestamp(timestamp)
|
116
|
+
@timestamp = timestamp
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_http_status(status_code)
|
120
|
+
status_code = status_code.to_i
|
121
|
+
set_data("status_code", status_code)
|
122
|
+
|
123
|
+
status =
|
124
|
+
if status_code >= 200 && status_code < 299
|
125
|
+
"ok"
|
126
|
+
else
|
127
|
+
STATUS_MAP[status_code]
|
128
|
+
end
|
129
|
+
set_status(status)
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_data(key, value)
|
133
|
+
@data[key] = value
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_tag(key, value)
|
137
|
+
@tags[key] = value
|
138
|
+
end
|
139
|
+
|
140
|
+
class SpanRecorder
|
141
|
+
attr_reader :max_length, :spans
|
142
|
+
|
143
|
+
def initialize(max_length)
|
144
|
+
@max_length = max_length
|
145
|
+
@spans = []
|
146
|
+
end
|
147
|
+
|
148
|
+
def add(span)
|
149
|
+
if @spans.count < @max_length
|
150
|
+
@spans << span
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Sentry
|
2
|
+
class Transaction < Span
|
3
|
+
SENTRY_TRACE_REGEXP = Regexp.new(
|
4
|
+
"^[ \t]*" + # whitespace
|
5
|
+
"([0-9a-f]{32})?" + # trace_id
|
6
|
+
"-?([0-9a-f]{16})?" + # span_id
|
7
|
+
"-?([01])?" + # sampled
|
8
|
+
"[ \t]*$" # whitespace
|
9
|
+
)
|
10
|
+
UNLABELD_NAME = "<unlabeled transaction>".freeze
|
11
|
+
MESSAGE_PREFIX = "[Tracing]"
|
12
|
+
|
13
|
+
attr_reader :name, :parent_sampled
|
14
|
+
|
15
|
+
def initialize(name: nil, parent_sampled: nil, **options)
|
16
|
+
super(**options)
|
17
|
+
|
18
|
+
@name = name
|
19
|
+
@parent_sampled = parent_sampled
|
20
|
+
set_span_recorder
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_sentry_trace(sentry_trace, **options)
|
24
|
+
return unless sentry_trace
|
25
|
+
|
26
|
+
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
|
27
|
+
trace_id, parent_span_id, sampled_flag = match[1..3]
|
28
|
+
|
29
|
+
sampled = sampled_flag != "0"
|
30
|
+
|
31
|
+
new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: sampled, **options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
hash = super
|
36
|
+
hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_initial_sample_desicion(sampling_context = {})
|
41
|
+
unless Sentry.configuration.tracing_enabled?
|
42
|
+
@sampled = false
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
return unless @sampled.nil?
|
47
|
+
|
48
|
+
transaction_description = generate_transaction_description
|
49
|
+
|
50
|
+
logger = Sentry.configuration.logger
|
51
|
+
sample_rate = Sentry.configuration.traces_sample_rate
|
52
|
+
traces_sampler = Sentry.configuration.traces_sampler
|
53
|
+
|
54
|
+
if traces_sampler.is_a?(Proc)
|
55
|
+
sampling_context = sampling_context.merge(
|
56
|
+
parent_sampled: @parent_sampled,
|
57
|
+
transaction_context: self.to_hash
|
58
|
+
)
|
59
|
+
|
60
|
+
sample_rate = traces_sampler.call(sampling_context)
|
61
|
+
end
|
62
|
+
|
63
|
+
unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Float) && sample_rate >= 0.0 && sample_rate <= 1.0)
|
64
|
+
@sampled = false
|
65
|
+
logger.warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
if sample_rate == 0.0 || sample_rate == false
|
70
|
+
@sampled = false
|
71
|
+
logger.debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
if sample_rate == true
|
76
|
+
@sampled = true
|
77
|
+
else
|
78
|
+
@sampled = Random.rand < sample_rate
|
79
|
+
end
|
80
|
+
|
81
|
+
if @sampled
|
82
|
+
logger.debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
|
83
|
+
else
|
84
|
+
logger.debug(
|
85
|
+
"#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def finish(hub: nil)
|
91
|
+
super() # Span#finish doesn't take arguments
|
92
|
+
|
93
|
+
if @name.nil?
|
94
|
+
@name = UNLABELD_NAME
|
95
|
+
end
|
96
|
+
|
97
|
+
return unless @sampled
|
98
|
+
|
99
|
+
hub ||= Sentry.get_current_hub
|
100
|
+
event = hub.current_client.event_from_transaction(self)
|
101
|
+
hub.capture_event(event)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def generate_transaction_description
|
107
|
+
result = op.nil? ? "" : "<#{@op}> "
|
108
|
+
result += "transaction"
|
109
|
+
result += " <#{@name}>" if @name
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|