sentry-ruby 5.23.0 → 5.27.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +1 -3
  4. data/README.md +11 -6
  5. data/Rakefile +7 -11
  6. data/lib/sentry/background_worker.rb +2 -3
  7. data/lib/sentry/backpressure_monitor.rb +1 -1
  8. data/lib/sentry/breadcrumb.rb +5 -4
  9. data/lib/sentry/check_in_event.rb +2 -1
  10. data/lib/sentry/client.rb +84 -4
  11. data/lib/sentry/configuration.rb +63 -2
  12. data/lib/sentry/debug_structured_logger.rb +94 -0
  13. data/lib/sentry/dsn.rb +32 -0
  14. data/lib/sentry/envelope/item.rb +1 -1
  15. data/lib/sentry/event.rb +2 -1
  16. data/lib/sentry/hub.rb +13 -1
  17. data/lib/sentry/interfaces/request.rb +1 -1
  18. data/lib/sentry/log_event.rb +206 -0
  19. data/lib/sentry/log_event_buffer.rb +75 -0
  20. data/lib/sentry/metrics/aggregator.rb +1 -1
  21. data/lib/sentry/profiler.rb +3 -2
  22. data/lib/sentry/propagation_context.rb +59 -22
  23. data/lib/sentry/scope.rb +13 -3
  24. data/lib/sentry/session_flusher.rb +1 -1
  25. data/lib/sentry/span.rb +4 -3
  26. data/lib/sentry/std_lib_logger.rb +50 -0
  27. data/lib/sentry/structured_logger.rb +138 -0
  28. data/lib/sentry/test_helper.rb +29 -0
  29. data/lib/sentry/threaded_periodic_worker.rb +3 -3
  30. data/lib/sentry/transaction.rb +30 -8
  31. data/lib/sentry/transport/debug_transport.rb +70 -0
  32. data/lib/sentry/transport/dummy_transport.rb +1 -0
  33. data/lib/sentry/transport/http_transport.rb +9 -5
  34. data/lib/sentry/transport.rb +17 -8
  35. data/lib/sentry/utils/logging_helper.rb +10 -3
  36. data/lib/sentry/utils/sample_rand.rb +97 -0
  37. data/lib/sentry/utils/uuid.rb +13 -0
  38. data/lib/sentry/vernier/profiler.rb +3 -2
  39. data/lib/sentry/version.rb +1 -1
  40. data/lib/sentry-ruby.rb +67 -4
  41. metadata +17 -9
data/lib/sentry/hub.rb CHANGED
@@ -120,7 +120,8 @@ module Sentry
120
120
 
121
121
  sampling_context = {
122
122
  transaction_context: transaction.to_hash,
123
- parent_sampled: transaction.parent_sampled
123
+ parent_sampled: transaction.parent_sampled,
124
+ parent_sample_rate: transaction.parent_sample_rate
124
125
  }
125
126
 
126
127
  sampling_context.merge!(custom_sampling_context)
@@ -216,6 +217,16 @@ module Sentry
216
217
  event.check_in_id
217
218
  end
218
219
 
220
+ def capture_log_event(message, **options)
221
+ return unless current_client
222
+
223
+ event = current_client.event_from_log(message, **options)
224
+
225
+ return unless event
226
+
227
+ current_client.buffer_log_event(event, current_scope)
228
+ end
229
+
219
230
  def capture_event(event, **options, &block)
220
231
  check_argument_type!(event, Sentry::Event)
221
232
 
@@ -347,6 +358,7 @@ module Sentry
347
358
  parent_span_id: propagation_context.parent_span_id,
348
359
  parent_sampled: propagation_context.parent_sampled,
349
360
  baggage: propagation_context.baggage,
361
+ sample_rand: propagation_context.sample_rand,
350
362
  **options
351
363
  )
352
364
  end
@@ -99,7 +99,7 @@ module Sentry
99
99
  # Rails adds objects to the Rack env that can sometimes raise exceptions
100
100
  # when `to_s` is called.
101
101
  # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134
102
- Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
102
+ Sentry.sdk_logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
103
103
  next
104
104
  end
105
105
  end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # Event type that represents a log entry with its attributes
5
+ #
6
+ # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload
7
+ class LogEvent
8
+ TYPE = "log"
9
+
10
+ DEFAULT_PARAMETERS = [].freeze
11
+ DEFAULT_ATTRIBUTES = {}.freeze
12
+
13
+ SERIALIZEABLE_ATTRIBUTES = %i[
14
+ level
15
+ body
16
+ timestamp
17
+ environment
18
+ release
19
+ server_name
20
+ trace_id
21
+ attributes
22
+ contexts
23
+ ]
24
+
25
+ SENTRY_ATTRIBUTES = {
26
+ "sentry.trace.parent_span_id" => :parent_span_id,
27
+ "sentry.environment" => :environment,
28
+ "sentry.release" => :release,
29
+ "sentry.address" => :server_name,
30
+ "sentry.sdk.name" => :sdk_name,
31
+ "sentry.sdk.version" => :sdk_version,
32
+ "sentry.message.template" => :template
33
+ }
34
+
35
+ USER_ATTRIBUTES = {
36
+ "user.id" => :user_id,
37
+ "user.name" => :user_username,
38
+ "user.email" => :user_email
39
+ }
40
+
41
+ LEVELS = %i[trace debug info warn error fatal].freeze
42
+
43
+ attr_accessor :level, :body, :template, :attributes, :user
44
+
45
+ attr_reader :configuration, *(SERIALIZEABLE_ATTRIBUTES - %i[level body attributes])
46
+
47
+ SERIALIZERS = %i[
48
+ attributes
49
+ body
50
+ level
51
+ parent_span_id
52
+ sdk_name
53
+ sdk_version
54
+ timestamp
55
+ trace_id
56
+ user_id
57
+ user_username
58
+ user_email
59
+ ].map { |name| [name, :"serialize_#{name}"] }.to_h
60
+
61
+ VALUE_TYPES = Hash.new("string").merge!({
62
+ TrueClass => "boolean",
63
+ FalseClass => "boolean",
64
+ Integer => "integer",
65
+ Float => "double"
66
+ }).freeze
67
+
68
+ TOKEN_REGEXP = /%\{(\w+)\}/
69
+
70
+ def initialize(configuration: Sentry.configuration, **options)
71
+ @configuration = configuration
72
+ @type = TYPE
73
+ @server_name = configuration.server_name
74
+ @environment = configuration.environment
75
+ @release = configuration.release
76
+ @timestamp = Sentry.utc_now
77
+ @level = options.fetch(:level)
78
+ @body = options[:body]
79
+ @template = @body if is_template?
80
+ @attributes = options[:attributes] || DEFAULT_ATTRIBUTES
81
+ @user = options[:user] || {}
82
+ @contexts = {}
83
+ end
84
+
85
+ def to_hash
86
+ SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo|
87
+ memo[name] = serialize(name)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def serialize(name)
94
+ serializer = SERIALIZERS[name]
95
+
96
+ if serializer
97
+ __send__(serializer)
98
+ else
99
+ public_send(name)
100
+ end
101
+ end
102
+
103
+ def serialize_level
104
+ level.to_s
105
+ end
106
+
107
+ def serialize_sdk_name
108
+ Sentry.sdk_meta["name"]
109
+ end
110
+
111
+ def serialize_sdk_version
112
+ Sentry.sdk_meta["version"]
113
+ end
114
+
115
+ def serialize_timestamp
116
+ timestamp.to_f
117
+ end
118
+
119
+ def serialize_trace_id
120
+ contexts.dig(:trace, :trace_id)
121
+ end
122
+
123
+ def serialize_parent_span_id
124
+ contexts.dig(:trace, :parent_span_id)
125
+ end
126
+
127
+ def serialize_body
128
+ if parameters.empty?
129
+ body
130
+ elsif parameters.is_a?(Hash)
131
+ body % parameters
132
+ else
133
+ sprintf(body, *parameters)
134
+ end
135
+ end
136
+
137
+ def serialize_user_id
138
+ user[:id]
139
+ end
140
+
141
+ def serialize_user_username
142
+ user[:username]
143
+ end
144
+
145
+ def serialize_user_email
146
+ user[:email]
147
+ end
148
+
149
+ def serialize_attributes
150
+ hash = {}
151
+
152
+ attributes.each do |key, value|
153
+ hash[key] = attribute_hash(value)
154
+ end
155
+
156
+ SENTRY_ATTRIBUTES.each do |key, name|
157
+ if (value = serialize(name))
158
+ hash[key] = attribute_hash(value)
159
+ end
160
+ end
161
+
162
+ USER_ATTRIBUTES.each do |key, name|
163
+ if (value = serialize(name))
164
+ hash[key] = value
165
+ end
166
+ end
167
+
168
+ hash
169
+ end
170
+
171
+ def attribute_hash(value)
172
+ { value: value, type: value_type(value) }
173
+ end
174
+
175
+ def value_type(value)
176
+ VALUE_TYPES[value.class]
177
+ end
178
+
179
+ def parameters
180
+ @parameters ||= begin
181
+ return DEFAULT_PARAMETERS unless template
182
+
183
+ parameters = template_tokens.empty? ?
184
+ attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens)
185
+
186
+ if parameters.is_a?(Hash)
187
+ parameters.each do |key, value|
188
+ attributes["sentry.message.parameter.#{key}"] = value
189
+ end
190
+ else
191
+ parameters.each_with_index do |param, index|
192
+ attributes["sentry.message.parameter.#{index}"] = param
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def template_tokens
199
+ @template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym)
200
+ end
201
+
202
+ def is_template?
203
+ body.include?("%s") || TOKEN_REGEXP.match?(body)
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/threaded_periodic_worker"
4
+
5
+ module Sentry
6
+ # LogEventBuffer buffers log 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 LogEventBuffer < ThreadedPeriodicWorker
12
+ FLUSH_INTERVAL = 5 # seconds
13
+ DEFAULT_MAX_EVENTS = 100
14
+
15
+ # @!visibility private
16
+ attr_reader :pending_events
17
+
18
+ def initialize(configuration, client)
19
+ super(configuration.sdk_logger, FLUSH_INTERVAL)
20
+
21
+ @client = client
22
+ @pending_events = []
23
+ @max_events = configuration.max_log_events || DEFAULT_MAX_EVENTS
24
+ @mutex = Mutex.new
25
+
26
+ log_debug("[Logging] Initialized buffer with max_events=#{@max_events}, flush_interval=#{FLUSH_INTERVAL}s")
27
+ end
28
+
29
+ def start
30
+ ensure_thread
31
+ self
32
+ end
33
+
34
+ def flush
35
+ @mutex.synchronize do
36
+ return if empty?
37
+
38
+ log_debug("[LogEventBuffer] flushing #{size} log events")
39
+
40
+ send_events
41
+ end
42
+
43
+ log_debug("[LogEventBuffer] flushed #{size} log events")
44
+
45
+ self
46
+ end
47
+ alias_method :run, :flush
48
+
49
+ def add_event(event)
50
+ raise ArgumentError, "expected a LogEvent, got #{event.class}" unless event.is_a?(LogEvent)
51
+
52
+ @mutex.synchronize do
53
+ @pending_events << event
54
+ send_events if size >= @max_events
55
+ end
56
+
57
+ self
58
+ end
59
+
60
+ def empty?
61
+ @pending_events.empty?
62
+ end
63
+
64
+ def size
65
+ @pending_events.size
66
+ end
67
+
68
+ private
69
+
70
+ def send_events
71
+ @client.send_logs(@pending_events)
72
+ @pending_events.clear
73
+ end
74
+ end
75
+ end
@@ -34,7 +34,7 @@ module Sentry
34
34
  attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
35
35
 
36
36
  def initialize(configuration, client)
37
- super(configuration.logger, FLUSH_INTERVAL)
37
+ super(configuration.sdk_logger, FLUSH_INTERVAL)
38
38
  @client = client
39
39
  @before_emit = configuration.metrics.before_emit
40
40
  @enable_code_locations = configuration.metrics.enable_code_locations
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "securerandom"
4
4
  require_relative "profiler/helpers"
5
+ require "sentry/utils/uuid"
5
6
 
6
7
  module Sentry
7
8
  class Profiler
@@ -17,7 +18,7 @@ module Sentry
17
18
  attr_reader :sampled, :started, :event_id
18
19
 
19
20
  def initialize(configuration)
20
- @event_id = SecureRandom.uuid.delete("-")
21
+ @event_id = Utils.uuid
21
22
  @started = false
22
23
  @sampled = nil
23
24
 
@@ -192,7 +193,7 @@ module Sentry
192
193
  private
193
194
 
194
195
  def log(message)
195
- Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
196
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
196
197
  end
197
198
 
198
199
  def record_lost_event(reason)
@@ -2,15 +2,15 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "sentry/baggage"
5
+ require "sentry/utils/uuid"
6
+ require "sentry/utils/sample_rand"
5
7
 
6
8
  module Sentry
7
9
  class PropagationContext
8
10
  SENTRY_TRACE_REGEXP = Regexp.new(
9
- "^[ \t]*" + # whitespace
10
- "([0-9a-f]{32})?" + # trace_id
11
+ "\\A([0-9a-f]{32})?" + # trace_id
11
12
  "-?([0-9a-f]{16})?" + # span_id
12
- "-?([01])?" + # sampled
13
- "[ \t]*$" # whitespace
13
+ "-?([01])?\\z" # sampled
14
14
  )
15
15
 
16
16
  # An uuid that can be used to identify a trace.
@@ -32,6 +32,53 @@ module Sentry
32
32
  # Please use the #get_baggage method for interfacing outside this class.
33
33
  # @return [Baggage, nil]
34
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
+ def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
57
+ return unless baggage&.items
58
+
59
+ sample_rand_str = baggage.items["sample_rand"]
60
+ return unless sample_rand_str
61
+
62
+ generator = Utils::SampleRand.new(trace_id: trace_id)
63
+ generator.generate_from_value(sample_rand_str)
64
+ end
65
+
66
+ def self.generate_sample_rand(baggage, trace_id, parent_sampled)
67
+ generator = Utils::SampleRand.new(trace_id: trace_id)
68
+
69
+ if baggage&.items && !parent_sampled.nil?
70
+ sample_rate_str = baggage.items["sample_rate"]
71
+ sample_rate = sample_rate_str&.to_f
72
+
73
+ if sample_rate && !parent_sampled.nil?
74
+ generator.generate_from_sampling_decision(parent_sampled, sample_rate)
75
+ else
76
+ generator.generate_from_trace_id
77
+ end
78
+ else
79
+ generator.generate_from_trace_id
80
+ end
81
+ end
35
82
 
36
83
  def initialize(scope, env = nil)
37
84
  @scope = scope
@@ -39,6 +86,7 @@ module Sentry
39
86
  @parent_sampled = nil
40
87
  @baggage = nil
41
88
  @incoming_trace = false
89
+ @sample_rand = nil
42
90
 
43
91
  if env
44
92
  sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
@@ -60,28 +108,17 @@ module Sentry
60
108
  Baggage.new({})
61
109
  end
62
110
 
111
+ @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
112
+
63
113
  @baggage.freeze!
64
114
  @incoming_trace = true
65
115
  end
66
116
  end
67
117
  end
68
118
 
69
- @trace_id ||= SecureRandom.uuid.delete("-")
70
- @span_id = SecureRandom.uuid.delete("-").slice(0, 16)
71
- end
72
-
73
- # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
74
- #
75
- # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
76
- # @return [Array, nil]
77
- def self.extract_sentry_trace(sentry_trace)
78
- match = SENTRY_TRACE_REGEXP.match(sentry_trace)
79
- return nil if match.nil?
80
-
81
- trace_id, parent_span_id, sampled_flag = match[1..3]
82
- parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
83
-
84
- [trace_id, parent_span_id, parent_sampled]
119
+ @trace_id ||= Utils.uuid
120
+ @span_id = Utils.uuid.slice(0, 16)
121
+ @sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
85
122
  end
86
123
 
87
124
  # Returns the trace context that can be used to embed in an Event.
@@ -122,10 +159,10 @@ module Sentry
122
159
 
123
160
  items = {
124
161
  "trace_id" => trace_id,
162
+ "sample_rand" => Utils::SampleRand.format(@sample_rand),
125
163
  "environment" => configuration.environment,
126
164
  "release" => configuration.release,
127
- "public_key" => configuration.dsn&.public_key,
128
- "user_segment" => @scope.user && @scope.user["segment"]
165
+ "public_key" => configuration.dsn&.public_key
129
166
  }
130
167
 
131
168
  items.compact!
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) || event.is_a?(LogEvent)
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,12 +60,22 @@ module Sentry
60
60
  event.attachments = attachments
61
61
  end
62
62
 
63
+ if event.is_a?(LogEvent)
64
+ event.user = user.merge(event.user)
65
+ end
66
+
63
67
  if span
64
68
  event.contexts[:trace] ||= span.get_trace_context
65
- event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
69
+
70
+ if event.respond_to?(:dynamic_sampling_context)
71
+ event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
72
+ end
66
73
  else
67
74
  event.contexts[:trace] ||= propagation_context.get_trace_context
68
- event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
75
+
76
+ if event.respond_to?(:dynamic_sampling_context)
77
+ event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
78
+ end
69
79
  end
70
80
 
71
81
  all_event_processors = self.class.global_event_processors + @event_processors
@@ -5,7 +5,7 @@ module Sentry
5
5
  FLUSH_INTERVAL = 60
6
6
 
7
7
  def initialize(configuration, client)
8
- super(configuration.logger, FLUSH_INTERVAL)
8
+ super(configuration.sdk_logger, FLUSH_INTERVAL)
9
9
  @client = client
10
10
  @pending_aggregates = {}
11
11
  @release = configuration.release
data/lib/sentry/span.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "sentry/metrics/local_aggregator"
5
+ require "sentry/utils/uuid"
5
6
 
6
7
  module Sentry
7
8
  class Span
@@ -127,8 +128,8 @@ module Sentry
127
128
  timestamp: nil,
128
129
  origin: nil
129
130
  )
130
- @trace_id = trace_id || SecureRandom.uuid.delete("-")
131
- @span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16)
131
+ @trace_id = trace_id || Utils.uuid
132
+ @span_id = span_id || Utils.uuid.slice(0, 16)
132
133
  @parent_span_id = parent_span_id
133
134
  @sampled = sampled
134
135
  @start_timestamp = start_timestamp || Sentry.utc_now.to_f
@@ -261,7 +262,7 @@ module Sentry
261
262
 
262
263
 
263
264
  # Sets the span's status.
264
- # @param satus [String] status of the span.
265
+ # @param status [String] status of the span.
265
266
  def set_status(status)
266
267
  @status = status
267
268
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # Ruby Logger support Add commentMore actions
5
+ # intercepts any logger instance and send the log to Sentry too.
6
+ module StdLibLogger
7
+ SEVERITY_MAP = {
8
+ 0 => :debug,
9
+ 1 => :info,
10
+ 2 => :warn,
11
+ 3 => :error,
12
+ 4 => :fatal
13
+ }.freeze
14
+
15
+ def add(severity, message = nil, progname = nil, &block)
16
+ result = super
17
+
18
+ return unless Sentry.initialized? && Sentry.get_current_hub
19
+
20
+ # exclude sentry SDK logs -- to prevent recursive log action,
21
+ # do not process internal logs again
22
+ if message.nil? && progname != Sentry::Logger::PROGNAME
23
+
24
+ # handle different nature of Ruby Logger class:
25
+ # inspo from Sentry::Breadcrumb::SentryLogger
26
+ if block_given?
27
+ message = yield
28
+ else
29
+ message = progname
30
+ end
31
+
32
+ message = message.to_s.strip
33
+
34
+ if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
35
+ Sentry.logger.send(method, message)
36
+ end
37
+ end
38
+
39
+ result
40
+ end
41
+ end
42
+ end
43
+
44
+ Sentry.register_patch(:logger) do |config|
45
+ if config.enable_logs
46
+ ::Logger.prepend(Sentry::StdLibLogger)
47
+ else
48
+ config.sdk_logger.warn(":logger patch enabled but `enable_logs` is turned off - skipping applying patch")
49
+ end
50
+ end