sentry-ruby 5.26.0 → 6.5.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/Gemfile +27 -5
- data/README.md +3 -3
- data/lib/sentry/background_worker.rb +1 -4
- data/lib/sentry/backtrace/line.rb +99 -0
- data/lib/sentry/backtrace.rb +44 -76
- data/lib/sentry/baggage.rb +2 -2
- data/lib/sentry/breadcrumb.rb +1 -1
- data/lib/sentry/breadcrumb_buffer.rb +2 -2
- data/lib/sentry/check_in_event.rb +2 -2
- data/lib/sentry/client.rb +59 -136
- data/lib/sentry/configuration.rb +206 -78
- data/lib/sentry/cron/monitor_check_ins.rb +3 -3
- data/lib/sentry/cron/monitor_config.rb +2 -2
- data/lib/sentry/cron/monitor_schedule.rb +2 -2
- data/lib/sentry/debug_structured_logger.rb +94 -0
- data/lib/sentry/dsn.rb +65 -1
- data/lib/sentry/envelope/item.rb +3 -3
- data/lib/sentry/error_event.rb +3 -3
- data/lib/sentry/event.rb +4 -10
- data/lib/sentry/exceptions.rb +3 -0
- data/lib/sentry/graphql.rb +1 -1
- data/lib/sentry/hub.rb +29 -5
- data/lib/sentry/interface.rb +1 -1
- data/lib/sentry/interfaces/exception.rb +2 -2
- data/lib/sentry/interfaces/request.rb +2 -0
- data/lib/sentry/interfaces/single_exception.rb +4 -4
- data/lib/sentry/interfaces/stacktrace.rb +3 -3
- data/lib/sentry/interfaces/stacktrace_builder.rb +0 -8
- data/lib/sentry/interfaces/threads.rb +2 -2
- data/lib/sentry/log_event.rb +33 -138
- data/lib/sentry/log_event_buffer.rb +13 -60
- data/lib/sentry/metric_event.rb +49 -0
- data/lib/sentry/metric_event_buffer.rb +28 -0
- data/lib/sentry/metrics.rb +47 -42
- data/lib/sentry/profiler.rb +4 -5
- data/lib/sentry/propagation_context.rb +101 -24
- data/lib/sentry/rack/capture_exceptions.rb +90 -2
- data/lib/sentry/release_detector.rb +1 -1
- data/lib/sentry/rspec.rb +1 -1
- data/lib/sentry/scope.rb +51 -18
- data/lib/sentry/sequel.rb +35 -0
- data/lib/sentry/span.rb +5 -17
- data/lib/sentry/std_lib_logger.rb +10 -1
- data/lib/sentry/telemetry_event_buffer.rb +130 -0
- data/lib/sentry/test_helper.rb +30 -0
- data/lib/sentry/transaction.rb +73 -95
- data/lib/sentry/transaction_event.rb +4 -9
- data/lib/sentry/transport/debug_transport.rb +70 -0
- data/lib/sentry/transport/dummy_transport.rb +1 -0
- data/lib/sentry/transport/http_transport.rb +16 -16
- data/lib/sentry/transport.rb +10 -7
- data/lib/sentry/utils/encoding_helper.rb +6 -0
- data/lib/sentry/utils/logging_helper.rb +25 -9
- data/lib/sentry/utils/sample_rand.rb +97 -0
- data/lib/sentry/utils/telemetry_attributes.rb +30 -0
- data/lib/sentry/vernier/profiler.rb +4 -3
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +57 -30
- data/sentry-ruby-core.gemspec +1 -1
- data/sentry-ruby.gemspec +2 -1
- metadata +31 -17
- data/lib/sentry/metrics/aggregator.rb +0 -248
- data/lib/sentry/metrics/configuration.rb +0 -47
- data/lib/sentry/metrics/counter_metric.rb +0 -25
- data/lib/sentry/metrics/distribution_metric.rb +0 -25
- data/lib/sentry/metrics/gauge_metric.rb +0 -35
- data/lib/sentry/metrics/local_aggregator.rb +0 -53
- data/lib/sentry/metrics/metric.rb +0 -19
- data/lib/sentry/metrics/set_metric.rb +0 -28
- data/lib/sentry/metrics/timing.rb +0 -51
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sentry/utils/telemetry_attributes"
|
|
4
|
+
|
|
5
|
+
module Sentry
|
|
6
|
+
class MetricEvent
|
|
7
|
+
include Sentry::Utils::TelemetryAttributes
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes
|
|
10
|
+
attr_writer :trace_id, :span_id, :attributes
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
name:,
|
|
14
|
+
type:,
|
|
15
|
+
value:,
|
|
16
|
+
unit: nil,
|
|
17
|
+
attributes: nil
|
|
18
|
+
)
|
|
19
|
+
@name = name
|
|
20
|
+
@type = type
|
|
21
|
+
@value = value
|
|
22
|
+
@unit = unit
|
|
23
|
+
@attributes = attributes || {}
|
|
24
|
+
|
|
25
|
+
@timestamp = Sentry.utc_now
|
|
26
|
+
@trace_id = nil
|
|
27
|
+
@span_id = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
name: @name,
|
|
33
|
+
type: @type,
|
|
34
|
+
value: @value,
|
|
35
|
+
unit: @unit,
|
|
36
|
+
timestamp: @timestamp.to_f,
|
|
37
|
+
trace_id: @trace_id,
|
|
38
|
+
span_id: @span_id,
|
|
39
|
+
attributes: serialize_attributes
|
|
40
|
+
}.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def serialize_attributes
|
|
46
|
+
@attributes.transform_values { |v| attribute_hash(v) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sentry/telemetry_event_buffer"
|
|
4
|
+
|
|
5
|
+
module Sentry
|
|
6
|
+
# MetricEventBuffer buffers metric events and sends them to Sentry in a single envelope.
|
|
7
|
+
#
|
|
8
|
+
# This is used internally by the `Sentry::Client`.
|
|
9
|
+
#
|
|
10
|
+
# @!visibility private
|
|
11
|
+
class MetricEventBuffer < TelemetryEventBuffer
|
|
12
|
+
DEFAULT_MAX_METRICS = 1000
|
|
13
|
+
MAX_METRICS_BEFORE_DROP = 10_000
|
|
14
|
+
|
|
15
|
+
def initialize(configuration, client)
|
|
16
|
+
super(
|
|
17
|
+
configuration,
|
|
18
|
+
client,
|
|
19
|
+
event_class: MetricEvent,
|
|
20
|
+
max_items: configuration.max_metric_events || DEFAULT_MAX_METRICS,
|
|
21
|
+
max_items_before_drop: MAX_METRICS_BEFORE_DROP,
|
|
22
|
+
envelope_type: "trace_metric",
|
|
23
|
+
envelope_content_type: "application/vnd.sentry.items.trace-metric+json",
|
|
24
|
+
before_send: configuration.before_send_metric
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/sentry/metrics.rb
CHANGED
|
@@ -1,55 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sentry/
|
|
4
|
-
require "sentry/metrics/counter_metric"
|
|
5
|
-
require "sentry/metrics/distribution_metric"
|
|
6
|
-
require "sentry/metrics/gauge_metric"
|
|
7
|
-
require "sentry/metrics/set_metric"
|
|
8
|
-
require "sentry/metrics/timing"
|
|
9
|
-
require "sentry/metrics/aggregator"
|
|
3
|
+
require "sentry/metric_event"
|
|
10
4
|
|
|
11
5
|
module Sentry
|
|
12
6
|
module Metrics
|
|
13
|
-
DURATION_UNITS = %w[nanosecond microsecond millisecond second minute hour day week]
|
|
14
|
-
INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte]
|
|
15
|
-
FRACTIONAL_UNITS = %w[ratio percent]
|
|
16
|
-
|
|
17
|
-
OP_NAME = "metric.timing"
|
|
18
|
-
SPAN_ORIGIN = "auto.metric.timing"
|
|
19
|
-
|
|
20
7
|
class << self
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
8
|
+
# Increments a counter metric
|
|
9
|
+
# @param name [String] the metric name
|
|
10
|
+
# @param value [Numeric] the value to increment by (default: 1)
|
|
11
|
+
# @param attributes [Hash, nil] additional attributes for the metric (optional)
|
|
12
|
+
# @return [void]
|
|
13
|
+
def count(name, value: 1, attributes: nil)
|
|
14
|
+
return unless Sentry.initialized?
|
|
15
|
+
|
|
16
|
+
Sentry.get_current_hub.capture_metric(
|
|
17
|
+
name: name,
|
|
18
|
+
type: :counter,
|
|
19
|
+
value: value,
|
|
20
|
+
attributes: attributes
|
|
21
|
+
)
|
|
31
22
|
end
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
# Records a gauge metric
|
|
25
|
+
# @param name [String] the metric name
|
|
26
|
+
# @param value [Numeric] the gauge value
|
|
27
|
+
# @param unit [String, nil] the metric unit (optional)
|
|
28
|
+
# @param attributes [Hash, nil] additional attributes for the metric (optional)
|
|
29
|
+
# @return [void]
|
|
30
|
+
def gauge(name, value, unit: nil, attributes: nil)
|
|
31
|
+
return unless Sentry.initialized?
|
|
32
|
+
|
|
33
|
+
Sentry.get_current_hub.capture_metric(
|
|
34
|
+
name: name,
|
|
35
|
+
type: :gauge,
|
|
36
|
+
value: value,
|
|
37
|
+
unit: unit,
|
|
38
|
+
attributes: attributes
|
|
39
|
+
)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
# Records a distribution metric
|
|
43
|
+
# @param name [String] the metric name
|
|
44
|
+
# @param value [Numeric] the distribution value
|
|
45
|
+
# @param unit [String, nil] the metric unit (optional)
|
|
46
|
+
# @param attributes [Hash, nil] additional attributes for the metric (optional)
|
|
47
|
+
# @return [void]
|
|
48
|
+
def distribution(name, value, unit: nil, attributes: nil)
|
|
49
|
+
return unless Sentry.initialized?
|
|
50
|
+
|
|
51
|
+
Sentry.get_current_hub.capture_metric(
|
|
52
|
+
name: name,
|
|
53
|
+
type: :distribution,
|
|
54
|
+
value: value,
|
|
55
|
+
unit: unit,
|
|
56
|
+
attributes: attributes
|
|
57
|
+
)
|
|
53
58
|
end
|
|
54
59
|
end
|
|
55
60
|
end
|
data/lib/sentry/profiler.rb
CHANGED
|
@@ -10,8 +10,6 @@ module Sentry
|
|
|
10
10
|
|
|
11
11
|
VERSION = "1"
|
|
12
12
|
PLATFORM = "ruby"
|
|
13
|
-
# 101 Hz in microseconds
|
|
14
|
-
DEFAULT_INTERVAL = 1e6 / 101
|
|
15
13
|
MICRO_TO_NANO_SECONDS = 1e3
|
|
16
14
|
MIN_SAMPLES_REQUIRED = 2
|
|
17
15
|
|
|
@@ -24,6 +22,7 @@ module Sentry
|
|
|
24
22
|
|
|
25
23
|
@profiling_enabled = defined?(StackProf) && configuration.profiling_enabled?
|
|
26
24
|
@profiles_sample_rate = configuration.profiles_sample_rate
|
|
25
|
+
@profiles_sample_interval = configuration.profiles_sample_interval
|
|
27
26
|
@project_root = configuration.project_root
|
|
28
27
|
@app_dirs_pattern = configuration.app_dirs_pattern
|
|
29
28
|
@in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
|
|
@@ -32,7 +31,7 @@ module Sentry
|
|
|
32
31
|
def start
|
|
33
32
|
return unless @sampled
|
|
34
33
|
|
|
35
|
-
@started = StackProf.start(interval:
|
|
34
|
+
@started = StackProf.start(interval: @profiles_sample_interval,
|
|
36
35
|
mode: :wall,
|
|
37
36
|
raw: true,
|
|
38
37
|
aggregate: false)
|
|
@@ -81,9 +80,9 @@ module Sentry
|
|
|
81
80
|
log("Discarding profile due to sampling decision") unless @sampled
|
|
82
81
|
end
|
|
83
82
|
|
|
84
|
-
def
|
|
83
|
+
def to_h
|
|
85
84
|
unless @sampled
|
|
86
|
-
record_lost_event(:sample_rate)
|
|
85
|
+
record_lost_event(:sample_rate) if @profiling_enabled
|
|
87
86
|
return {}
|
|
88
87
|
end
|
|
89
88
|
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "sentry/baggage"
|
|
5
5
|
require "sentry/utils/uuid"
|
|
6
|
+
require "sentry/utils/sample_rand"
|
|
6
7
|
|
|
7
8
|
module Sentry
|
|
8
9
|
class PropagationContext
|
|
9
10
|
SENTRY_TRACE_REGEXP = Regexp.new(
|
|
10
|
-
"
|
|
11
|
-
"([0-9a-f]{32})?" + # trace_id
|
|
11
|
+
"\\A([0-9a-f]{32})?" + # trace_id
|
|
12
12
|
"-?([0-9a-f]{16})?" + # span_id
|
|
13
|
-
"-?([01])
|
|
14
|
-
"[ \t]*$" # whitespace
|
|
13
|
+
"-?([01])?\\z" # sampled
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
# An uuid that can be used to identify a trace.
|
|
@@ -33,6 +32,91 @@ module Sentry
|
|
|
33
32
|
# Please use the #get_baggage method for interfacing outside this class.
|
|
34
33
|
# @return [Baggage, nil]
|
|
35
34
|
attr_reader :baggage
|
|
35
|
+
# The propagated random value used for sampling decisions.
|
|
36
|
+
# @return [Float, nil]
|
|
37
|
+
attr_reader :sample_rand
|
|
38
|
+
|
|
39
|
+
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
|
|
40
|
+
#
|
|
41
|
+
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
|
|
42
|
+
# @return [Array, nil]
|
|
43
|
+
def self.extract_sentry_trace(sentry_trace)
|
|
44
|
+
value = sentry_trace.to_s.strip
|
|
45
|
+
return if value.empty?
|
|
46
|
+
|
|
47
|
+
match = SENTRY_TRACE_REGEXP.match(value)
|
|
48
|
+
return if match.nil?
|
|
49
|
+
|
|
50
|
+
trace_id, parent_span_id, sampled_flag = match[1..3]
|
|
51
|
+
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
|
|
52
|
+
|
|
53
|
+
[trace_id, parent_span_id, parent_sampled]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Determines whether we should continue an incoming trace based on org_id matching
|
|
57
|
+
# and the strict_trace_continuation configuration option.
|
|
58
|
+
#
|
|
59
|
+
# @param incoming_baggage [Baggage] the baggage from the incoming request
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def self.should_continue_trace?(incoming_baggage)
|
|
62
|
+
return true unless Sentry.initialized?
|
|
63
|
+
|
|
64
|
+
configuration = Sentry.configuration
|
|
65
|
+
sdk_org_id = configuration.effective_org_id
|
|
66
|
+
baggage_org_id = incoming_baggage.items["org_id"]
|
|
67
|
+
|
|
68
|
+
# Mismatched org IDs always start a new trace regardless of strict mode
|
|
69
|
+
if sdk_org_id && baggage_org_id && sdk_org_id != baggage_org_id
|
|
70
|
+
Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
|
|
71
|
+
"Starting a new trace because org IDs don't match (incoming baggage org_id: #{baggage_org_id}, SDK org_id: #{sdk_org_id})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return true unless configuration.strict_trace_continuation
|
|
78
|
+
|
|
79
|
+
# In strict mode, both must be present and match (unless both are missing)
|
|
80
|
+
if sdk_org_id.nil? && baggage_org_id.nil?
|
|
81
|
+
true
|
|
82
|
+
elsif sdk_org_id.nil? || baggage_org_id.nil?
|
|
83
|
+
Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
|
|
84
|
+
"Starting a new trace because strict trace continuation is enabled and one org ID is missing " \
|
|
85
|
+
"(incoming baggage org_id: #{baggage_org_id.inspect}, SDK org_id: #{sdk_org_id.inspect})"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
false
|
|
89
|
+
else
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
|
|
95
|
+
return unless baggage&.items
|
|
96
|
+
|
|
97
|
+
sample_rand_str = baggage.items["sample_rand"]
|
|
98
|
+
return unless sample_rand_str
|
|
99
|
+
|
|
100
|
+
generator = Utils::SampleRand.new(trace_id: trace_id)
|
|
101
|
+
generator.generate_from_value(sample_rand_str)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.generate_sample_rand(baggage, trace_id, parent_sampled)
|
|
105
|
+
generator = Utils::SampleRand.new(trace_id: trace_id)
|
|
106
|
+
|
|
107
|
+
if baggage&.items && !parent_sampled.nil?
|
|
108
|
+
sample_rate_str = baggage.items["sample_rate"]
|
|
109
|
+
sample_rate = sample_rate_str&.to_f
|
|
110
|
+
|
|
111
|
+
if sample_rate && !parent_sampled.nil?
|
|
112
|
+
generator.generate_from_sampling_decision(parent_sampled, sample_rate)
|
|
113
|
+
else
|
|
114
|
+
generator.generate_from_trace_id
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
generator.generate_from_trace_id
|
|
118
|
+
end
|
|
119
|
+
end
|
|
36
120
|
|
|
37
121
|
def initialize(scope, env = nil)
|
|
38
122
|
@scope = scope
|
|
@@ -40,6 +124,7 @@ module Sentry
|
|
|
40
124
|
@parent_sampled = nil
|
|
41
125
|
@baggage = nil
|
|
42
126
|
@incoming_trace = false
|
|
127
|
+
@sample_rand = nil
|
|
43
128
|
|
|
44
129
|
if env
|
|
45
130
|
sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
|
|
@@ -49,9 +134,7 @@ module Sentry
|
|
|
49
134
|
sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
|
|
50
135
|
|
|
51
136
|
if sentry_trace_data
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@baggage =
|
|
137
|
+
incoming_baggage =
|
|
55
138
|
if baggage_header && !baggage_header.empty?
|
|
56
139
|
Baggage.from_incoming_header(baggage_header)
|
|
57
140
|
else
|
|
@@ -61,28 +144,20 @@ module Sentry
|
|
|
61
144
|
Baggage.new({})
|
|
62
145
|
end
|
|
63
146
|
|
|
64
|
-
|
|
65
|
-
|
|
147
|
+
if self.class.should_continue_trace?(incoming_baggage)
|
|
148
|
+
@trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
|
|
149
|
+
@baggage = incoming_baggage
|
|
150
|
+
@sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
|
|
151
|
+
@baggage.freeze!
|
|
152
|
+
@incoming_trace = true
|
|
153
|
+
end
|
|
66
154
|
end
|
|
67
155
|
end
|
|
68
156
|
end
|
|
69
157
|
|
|
70
158
|
@trace_id ||= Utils.uuid
|
|
71
159
|
@span_id = Utils.uuid.slice(0, 16)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
|
|
75
|
-
#
|
|
76
|
-
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
|
|
77
|
-
# @return [Array, nil]
|
|
78
|
-
def self.extract_sentry_trace(sentry_trace)
|
|
79
|
-
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
|
|
80
|
-
return nil if match.nil?
|
|
81
|
-
|
|
82
|
-
trace_id, parent_span_id, sampled_flag = match[1..3]
|
|
83
|
-
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
|
|
84
|
-
|
|
85
|
-
[trace_id, parent_span_id, parent_sampled]
|
|
160
|
+
@sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
|
|
86
161
|
end
|
|
87
162
|
|
|
88
163
|
# Returns the trace context that can be used to embed in an Event.
|
|
@@ -123,9 +198,11 @@ module Sentry
|
|
|
123
198
|
|
|
124
199
|
items = {
|
|
125
200
|
"trace_id" => trace_id,
|
|
201
|
+
"sample_rand" => Utils::SampleRand.format(@sample_rand),
|
|
126
202
|
"environment" => configuration.environment,
|
|
127
203
|
"release" => configuration.release,
|
|
128
|
-
"public_key" => configuration.dsn&.public_key
|
|
204
|
+
"public_key" => configuration.dsn&.public_key,
|
|
205
|
+
"org_id" => configuration.effective_org_id
|
|
129
206
|
}
|
|
130
207
|
|
|
131
208
|
items.compact!
|
|
@@ -33,7 +33,7 @@ module Sentry
|
|
|
33
33
|
raise # Don't capture Sentry errors
|
|
34
34
|
rescue Exception => e
|
|
35
35
|
capture_exception(e, env)
|
|
36
|
-
finish_transaction(transaction,
|
|
36
|
+
finish_transaction(transaction, status_code_for_exception(e))
|
|
37
37
|
raise
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -72,7 +72,16 @@ module Sentry
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
transaction = Sentry.continue_trace(env, **options)
|
|
75
|
-
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
|
75
|
+
transaction = Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
|
76
|
+
attach_queue_time(transaction, env)
|
|
77
|
+
transaction
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def attach_queue_time(transaction, env)
|
|
81
|
+
return unless transaction
|
|
82
|
+
return unless (queue_time = extract_queue_time(env))
|
|
83
|
+
|
|
84
|
+
transaction.set_data(Span::DataConventions::HTTP_QUEUE_TIME_MS, queue_time)
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
|
|
@@ -86,6 +95,85 @@ module Sentry
|
|
|
86
95
|
def mechanism
|
|
87
96
|
Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
|
|
88
97
|
end
|
|
98
|
+
|
|
99
|
+
# Extracts queue time from the request environment.
|
|
100
|
+
# Calculates the time (in milliseconds) the request spent waiting in the
|
|
101
|
+
# web server queue before processing began.
|
|
102
|
+
#
|
|
103
|
+
# Subtracts puma.request_body_wait to account for time spent waiting for
|
|
104
|
+
# slow clients to send the request body, isolating actual queue time.
|
|
105
|
+
# See: https://github.com/puma/puma/blob/master/docs/architecture.md
|
|
106
|
+
#
|
|
107
|
+
# @param env [Hash] Rack env
|
|
108
|
+
# @return [Float, nil] queue time in milliseconds or nil
|
|
109
|
+
def extract_queue_time(env)
|
|
110
|
+
return unless Sentry.configuration&.capture_queue_time
|
|
111
|
+
|
|
112
|
+
header_value = env["HTTP_X_REQUEST_START"]
|
|
113
|
+
return unless header_value
|
|
114
|
+
|
|
115
|
+
request_start = parse_request_start_header(header_value)
|
|
116
|
+
return unless request_start
|
|
117
|
+
|
|
118
|
+
total_time_ms = ((Time.now.to_f - request_start) * 1000).round(2)
|
|
119
|
+
|
|
120
|
+
# reject negative (clock skew between proxy & app server)
|
|
121
|
+
return unless total_time_ms >= 0
|
|
122
|
+
|
|
123
|
+
puma_wait_ms = env["puma.request_body_wait"]
|
|
124
|
+
puma_wait_ms = puma_wait_ms.to_f if puma_wait_ms.is_a?(String)
|
|
125
|
+
|
|
126
|
+
if puma_wait_ms && puma_wait_ms > 0
|
|
127
|
+
queue_time_ms = total_time_ms - puma_wait_ms
|
|
128
|
+
queue_time_ms >= 0 ? queue_time_ms : 0.0 # more sanity check
|
|
129
|
+
else
|
|
130
|
+
total_time_ms
|
|
131
|
+
end
|
|
132
|
+
rescue StandardError
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parses X-Request-Start header value to extract a timestamp.
|
|
136
|
+
# Supports multiple formats:
|
|
137
|
+
# - Nginx: "t=1234567890.123" (seconds with decimal)
|
|
138
|
+
# - Heroku, HAProxy 1.9+: "t=1234567890123456" (microseconds)
|
|
139
|
+
# - HAProxy < 1.9: "t=1234567890" (seconds)
|
|
140
|
+
# - Generic: "1234567890.123" (raw timestamp)
|
|
141
|
+
#
|
|
142
|
+
# @param header_value [String] The X-Request-Start header value
|
|
143
|
+
# @return [Float, nil] Timestamp in seconds since epoch or nil
|
|
144
|
+
def parse_request_start_header(header_value)
|
|
145
|
+
return unless header_value
|
|
146
|
+
|
|
147
|
+
# Take the first value if comma-separated (multiple headers collapsed by a proxy)
|
|
148
|
+
# and strip surrounding whitespace from each token
|
|
149
|
+
raw = header_value.split(",").first.to_s.strip
|
|
150
|
+
|
|
151
|
+
timestamp = if raw.start_with?("t=")
|
|
152
|
+
value = raw[2..-1].strip
|
|
153
|
+
return nil unless value.match?(/\A\d+(?:\.\d+)?\z/)
|
|
154
|
+
value.to_f
|
|
155
|
+
elsif raw.match?(/\A\d+(?:\.\d+)?\z/)
|
|
156
|
+
raw.to_f
|
|
157
|
+
else
|
|
158
|
+
return
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# normalize: timestamps can be in seconds, milliseconds or microseconds
|
|
162
|
+
# any timestamp > 10 trillion = microseconds
|
|
163
|
+
if timestamp > 10_000_000_000_000
|
|
164
|
+
timestamp / 1_000_000.0
|
|
165
|
+
# timestamp > 10 billion & < 10 trillion = milliseconds
|
|
166
|
+
elsif timestamp > 10_000_000_000
|
|
167
|
+
timestamp / 1_000.0
|
|
168
|
+
else
|
|
169
|
+
timestamp # assume seconds
|
|
170
|
+
end
|
|
171
|
+
rescue StandardError
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def status_code_for_exception(exception)
|
|
175
|
+
500
|
|
176
|
+
end
|
|
89
177
|
end
|
|
90
178
|
end
|
|
91
179
|
end
|
data/lib/sentry/rspec.rb
CHANGED
|
@@ -70,7 +70,7 @@ RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts|
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def dump_events(sentry_events)
|
|
73
|
-
sentry_events.map(
|
|
73
|
+
sentry_events.map(&:to_h).map do |hash|
|
|
74
74
|
hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) }
|
|
75
75
|
end.map do |hash|
|
|
76
76
|
JSON.pretty_generate(hash)
|
data/lib/sentry/scope.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Sentry
|
|
|
46
46
|
# @param hint [Hash] the hint data that'll be passed to event processors.
|
|
47
47
|
# @return [Event]
|
|
48
48
|
def apply_to_event(event, hint = nil)
|
|
49
|
-
unless event.is_a?(CheckInEvent)
|
|
49
|
+
unless event.is_a?(CheckInEvent)
|
|
50
50
|
event.tags = tags.merge(event.tags)
|
|
51
51
|
event.user = user.merge(event.user)
|
|
52
52
|
event.extra = extra.merge(event.extra)
|
|
@@ -60,23 +60,10 @@ module Sentry
|
|
|
60
60
|
event.attachments = attachments
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if span
|
|
68
|
-
event.contexts[:trace] ||= span.get_trace_context
|
|
69
|
-
|
|
70
|
-
if event.respond_to?(:dynamic_sampling_context)
|
|
71
|
-
event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
|
|
72
|
-
end
|
|
73
|
-
else
|
|
74
|
-
event.contexts[:trace] ||= propagation_context.get_trace_context
|
|
75
|
-
|
|
76
|
-
if event.respond_to?(:dynamic_sampling_context)
|
|
77
|
-
event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
|
|
78
|
-
end
|
|
79
|
-
end
|
|
63
|
+
trace_context = get_trace_context
|
|
64
|
+
dynamic_sampling_context = trace_context.delete(:dynamic_sampling_context)
|
|
65
|
+
event.contexts[:trace] ||= trace_context
|
|
66
|
+
event.dynamic_sampling_context ||= dynamic_sampling_context
|
|
80
67
|
|
|
81
68
|
all_event_processors = self.class.global_event_processors + @event_processors
|
|
82
69
|
|
|
@@ -89,6 +76,37 @@ module Sentry
|
|
|
89
76
|
event
|
|
90
77
|
end
|
|
91
78
|
|
|
79
|
+
# A leaner version of apply_to_event that applies to
|
|
80
|
+
# lightweight payloads like Logs and Metrics.
|
|
81
|
+
#
|
|
82
|
+
# Adds trace_id, span_id, user from the scope and default attributes from configuration.
|
|
83
|
+
#
|
|
84
|
+
# @param telemetry [MetricEvent, LogEvent] the telemetry event to apply scope context to
|
|
85
|
+
# @return [MetricEvent, LogEvent] the telemetry event with scope context applied
|
|
86
|
+
def apply_to_telemetry(telemetry)
|
|
87
|
+
# TODO-neel when new scope set_attribute api is added: add them here
|
|
88
|
+
trace_context = get_trace_context
|
|
89
|
+
telemetry.trace_id = trace_context[:trace_id]
|
|
90
|
+
telemetry.span_id = trace_context[:span_id]
|
|
91
|
+
|
|
92
|
+
configuration = Sentry.configuration
|
|
93
|
+
return telemetry unless configuration
|
|
94
|
+
|
|
95
|
+
telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"]
|
|
96
|
+
telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"]
|
|
97
|
+
telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment
|
|
98
|
+
telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
|
|
99
|
+
telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
|
|
100
|
+
|
|
101
|
+
unless user.empty?
|
|
102
|
+
telemetry.attributes["user.id"] ||= user[:id] if user[:id]
|
|
103
|
+
telemetry.attributes["user.name"] ||= user[:username] if user[:username]
|
|
104
|
+
telemetry.attributes["user.email"] ||= user[:email] if user[:email]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
telemetry
|
|
108
|
+
end
|
|
109
|
+
|
|
92
110
|
# Adds the breadcrumb to the scope's breadcrumbs buffer.
|
|
93
111
|
# @param breadcrumb [Breadcrumb]
|
|
94
112
|
# @return [void]
|
|
@@ -117,6 +135,7 @@ module Sentry
|
|
|
117
135
|
copy.session = session.deep_dup
|
|
118
136
|
copy.propagation_context = propagation_context.deep_dup
|
|
119
137
|
copy.attachments = attachments.dup
|
|
138
|
+
copy.event_processors = event_processors.dup
|
|
120
139
|
copy
|
|
121
140
|
end
|
|
122
141
|
|
|
@@ -278,6 +297,20 @@ module Sentry
|
|
|
278
297
|
span
|
|
279
298
|
end
|
|
280
299
|
|
|
300
|
+
# Returns the trace context for this scope.
|
|
301
|
+
# Prioritizes external propagation context (from OTel) over local propagation context.
|
|
302
|
+
# @return [Hash]
|
|
303
|
+
def get_trace_context
|
|
304
|
+
if span
|
|
305
|
+
span.get_trace_context.merge(dynamic_sampling_context: span.get_dynamic_sampling_context)
|
|
306
|
+
elsif (external_context = Sentry.get_external_propagation_context)
|
|
307
|
+
trace_id, span_id = external_context
|
|
308
|
+
{ trace_id: trace_id, span_id: span_id }
|
|
309
|
+
else
|
|
310
|
+
propagation_context.get_trace_context.merge(dynamic_sampling_context: propagation_context.get_dynamic_sampling_context)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
281
314
|
# Sets the scope's fingerprint attribute.
|
|
282
315
|
# @param fingerprint [Array]
|
|
283
316
|
# @return [Array]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sentry
|
|
4
|
+
module Sequel
|
|
5
|
+
OP_NAME = "db.sql.sequel"
|
|
6
|
+
SPAN_ORIGIN = "auto.db.sequel"
|
|
7
|
+
|
|
8
|
+
# Sequel Database extension module that instruments queries
|
|
9
|
+
module DatabaseExtension
|
|
10
|
+
def log_connection_yield(sql, conn, args = nil)
|
|
11
|
+
return super unless Sentry.initialized?
|
|
12
|
+
|
|
13
|
+
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span|
|
|
14
|
+
result = super
|
|
15
|
+
|
|
16
|
+
if span
|
|
17
|
+
span.set_description(sql)
|
|
18
|
+
span.set_data(Span::DataConventions::DB_SYSTEM, database_type.to_s)
|
|
19
|
+
span.set_data(Span::DataConventions::DB_NAME, opts[:database]) if opts[:database]
|
|
20
|
+
span.set_data(Span::DataConventions::SERVER_ADDRESS, opts[:host]) if opts[:host]
|
|
21
|
+
span.set_data(Span::DataConventions::SERVER_PORT, opts[:port]) if opts[:port]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
::Sequel::Database.register_extension(:sentry, Sentry::Sequel::DatabaseExtension)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Sentry.register_patch(:sequel) do
|
|
34
|
+
::Sequel::Database.extension(:sentry) if defined?(::Sequel::Database)
|
|
35
|
+
end
|