sentry-ruby 6.2.0 → 6.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 768b3e936767873916ec042d50b4beec38446376593c8bd177ce8a94c0aa7140
4
- data.tar.gz: c55f461f61ee4794134b0ebbeff58dc6c354de67ed9cd6cc5925335e20f31685
3
+ metadata.gz: ea228d904661a633e6bdfd3eb0ea4dc6ac129a0fed8f4a0e378e9a6e84de30e0
4
+ data.tar.gz: 71c6db91301a24a1b05afa798a249de12b31a5d68c45d16f138f73c0999b3183
5
5
  SHA512:
6
- metadata.gz: '02192b9d9c6bf6a1c6698c1c793a5048373b31bcbee7d3f4d890fbb3d5969d3548d261eef133c35d14452fd8b366feddc58557779e25a5c0a43d23fe005cc900'
7
- data.tar.gz: 8ff1c960e8e4f5f028d1243d0815eead580f259863b2740e3a6159e18238986cd9517203d7bd4c9a61b4389955068ff7ccb77f689e08baa09ac30c6d86d15681
6
+ metadata.gz: 07adab1cfe625b662c7cc89867225f40f07a36b0b73b70c916969ad5bac6ec1516bda791b767398e0b48246e5f23e31601d954cec9f724a79d33980e45d6e4d5
7
+ data.tar.gz: 5072c6873787d7e070050df7aeeb3e9040ca3add4c04f1c4e21c63d12fb241ee6615eced7f71fac5edeee4381d60272069bdc38ce967033ead0e2e76894b2573
data/Gemfile CHANGED
@@ -7,8 +7,11 @@ eval_gemfile "../Gemfile.dev"
7
7
 
8
8
  gem "sentry-ruby", path: "./"
9
9
 
10
+ ruby_version = Gem::Version.new(RUBY_VERSION)
11
+
10
12
  rack_version = ENV["RACK_VERSION"]
11
13
  rack_version = "3.0.0" if rack_version.nil?
14
+
12
15
  gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0"
13
16
 
14
17
  redis_rb_version = ENV.fetch("REDIS_RB_VERSION", "5.0")
@@ -32,3 +35,24 @@ gem "webrick"
32
35
  gem "faraday"
33
36
  gem "excon"
34
37
  gem "webmock"
38
+
39
+ group :sequel do
40
+ gem "sequel"
41
+
42
+ sqlite_version = if ruby_version >= Gem::Version.new("3.2")
43
+ "2.1.0"
44
+ elsif ruby_version >= Gem::Version.new("3.0")
45
+ "1.4.0"
46
+ else
47
+ "1.3.0"
48
+ end
49
+
50
+ platform :ruby do
51
+ gem "sqlite3", "~> #{sqlite_version}"
52
+ end
53
+
54
+ platform :jruby do
55
+ gem "activerecord-jdbcmysql-adapter"
56
+ gem "jdbc-sqlite3"
57
+ end
58
+ end
data/README.md CHANGED
@@ -105,7 +105,7 @@ To learn more about sampling transactions, please visit the [official documentat
105
105
 
106
106
  * [![Ruby docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=ruby%20docs)](https://docs.sentry.io/platforms/ruby/)
107
107
  * [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks)
108
- * [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K)
108
+ * [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/sentry)
109
109
  * [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry)
110
110
  * [![X Follow](https://img.shields.io/twitter/follow/sentry?label=sentry&style=social)](https://x.com/intent/follow?screen_name=sentry)
111
111
 
data/lib/sentry/client.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "sentry/transport"
4
4
  require "sentry/log_event"
5
5
  require "sentry/log_event_buffer"
6
+ require "sentry/metric_event"
7
+ require "sentry/metric_event_buffer"
6
8
  require "sentry/utils/uuid"
7
9
  require "sentry/utils/encoding_helper"
8
10
 
@@ -21,6 +23,9 @@ module Sentry
21
23
  # @!visibility private
22
24
  attr_reader :log_event_buffer
23
25
 
26
+ # @!visibility private
27
+ attr_reader :metric_event_buffer
28
+
24
29
  # @!macro configuration
25
30
  attr_reader :configuration
26
31
 
@@ -44,7 +49,11 @@ module Sentry
44
49
  @spotlight_transport = SpotlightTransport.new(configuration) if configuration.spotlight
45
50
 
46
51
  if configuration.enable_logs
47
- @log_event_buffer = LogEventBuffer.new(configuration, self).start
52
+ @log_event_buffer = LogEventBuffer.new(configuration, self)
53
+ end
54
+
55
+ if configuration.enable_metrics
56
+ @metric_event_buffer = MetricEventBuffer.new(configuration, self)
48
57
  end
49
58
  end
50
59
 
@@ -98,7 +107,17 @@ module Sentry
98
107
  # @return [LogEvent]
99
108
  def buffer_log_event(event, scope)
100
109
  return unless event.is_a?(LogEvent)
101
- @log_event_buffer.add_event(scope.apply_to_event(event))
110
+ @log_event_buffer.add_item(scope.apply_to_telemetry(event))
111
+ event
112
+ end
113
+
114
+ # Buffer a metric event to be sent later with other metrics in a single envelope
115
+ # @param event [MetricEvent] the metric event to be buffered
116
+ # @return [MetricEvent]
117
+ def buffer_metric_event(event, scope)
118
+ return unless event.is_a?(MetricEvent)
119
+ event = scope.apply_to_telemetry(event)
120
+ @metric_event_buffer.add_item(event)
102
121
  event
103
122
  end
104
123
 
@@ -115,6 +134,7 @@ module Sentry
115
134
  transport.flush if configuration.sending_to_dsn_allowed?
116
135
  spotlight_transport.flush if spotlight_transport
117
136
  @log_event_buffer&.flush
137
+ @metric_event_buffer&.flush
118
138
  end
119
139
 
120
140
  # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting.
@@ -275,53 +295,6 @@ module Sentry
275
295
  raise
276
296
  end
277
297
 
278
- # Send an envelope with batched logs
279
- # @param log_events [Array<LogEvent>] the log events to be sent
280
- # @api private
281
- # @return [void]
282
- def send_logs(log_events)
283
- envelope = Envelope.new(
284
- event_id: Sentry::Utils.uuid,
285
- sent_at: Sentry.utc_now.iso8601,
286
- dsn: configuration.dsn,
287
- sdk: Sentry.sdk_meta
288
- )
289
-
290
- discarded_count = 0
291
- envelope_items = []
292
-
293
- if configuration.before_send_log
294
- log_events.each do |log_event|
295
- processed_log_event = configuration.before_send_log.call(log_event)
296
-
297
- if processed_log_event
298
- envelope_items << processed_log_event.to_h
299
- else
300
- discarded_count += 1
301
- end
302
- end
303
-
304
- envelope_items
305
- else
306
- envelope_items = log_events.map(&:to_h)
307
- end
308
-
309
- envelope.add_item(
310
- {
311
- type: "log",
312
- item_count: envelope_items.size,
313
- content_type: "application/vnd.sentry.items.log+json"
314
- },
315
- { items: envelope_items }
316
- )
317
-
318
- send_envelope(envelope)
319
-
320
- unless discarded_count.zero?
321
- transport.record_lost_event(:before_send, "log_item", num: discarded_count)
322
- end
323
- end
324
-
325
298
  # Send an envelope directly to Sentry.
326
299
  # @param envelope [Envelope] the envelope to be sent.
327
300
  # @return [void]
@@ -14,6 +14,7 @@ require "sentry/interfaces/stacktrace_builder"
14
14
  require "sentry/logger"
15
15
  require "sentry/structured_logger"
16
16
  require "sentry/log_event_buffer"
17
+ require "sentry/metric_event_buffer"
17
18
 
18
19
  module Sentry
19
20
  class Configuration
@@ -337,6 +338,32 @@ module Sentry
337
338
  # @return [Integer]
338
339
  attr_accessor :max_log_events
339
340
 
341
+ # Enable metrics collection, defaults to true
342
+ # @return [Boolean]
343
+ attr_accessor :enable_metrics
344
+
345
+ # Maximum number of metric events to buffer before sending
346
+ # @return [Integer]
347
+ attr_accessor :max_metric_events
348
+
349
+ # Optional Proc, called before sending a metric
350
+ # @example
351
+ # config.before_send_metric = lambda do |metric|
352
+ # # return nil to drop the metric
353
+ # metric
354
+ # end
355
+ # @return [Proc, nil]
356
+ attr_reader :before_send_metric
357
+
358
+ # Optional Proc, called to filter log messages before sending to Sentry
359
+ # @example
360
+ # config.std_lib_logger_filter = lambda do |logger, message, level|
361
+ # # Only send error and fatal logs to Sentry
362
+ # [:error, :fatal].include?(level)
363
+ # end
364
+ # @return [Proc, nil]
365
+ attr_reader :std_lib_logger_filter
366
+
340
367
  # these are not config options
341
368
  # @!visibility private
342
369
  attr_reader :errors, :gem_specs
@@ -499,9 +526,12 @@ module Sentry
499
526
  self.before_send_transaction = nil
500
527
  self.before_send_check_in = nil
501
528
  self.before_send_log = nil
529
+ self.before_send_metric = nil
530
+ self.std_lib_logger_filter = nil
502
531
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
503
532
  self.traces_sampler = nil
504
533
  self.enable_logs = false
534
+ self.enable_metrics = true
505
535
 
506
536
  self.profiler_class = Sentry::Profiler
507
537
  self.profiles_sample_interval = DEFAULT_PROFILES_SAMPLE_INTERVAL
@@ -512,6 +542,7 @@ module Sentry
512
542
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
513
543
 
514
544
  self.max_log_events = LogEventBuffer::DEFAULT_MAX_EVENTS
545
+ self.max_metric_events = MetricEventBuffer::DEFAULT_MAX_METRICS
515
546
 
516
547
  run_callbacks(:after, :initialize)
517
548
 
@@ -581,12 +612,24 @@ module Sentry
581
612
  @before_send_check_in = value
582
613
  end
583
614
 
615
+ def before_send_metric=(value)
616
+ check_callable!("before_send_metric", value)
617
+
618
+ @before_send_metric = value
619
+ end
620
+
584
621
  def before_breadcrumb=(value)
585
622
  check_callable!("before_breadcrumb", value)
586
623
 
587
624
  @before_breadcrumb = value
588
625
  end
589
626
 
627
+ def std_lib_logger_filter=(value)
628
+ check_callable!("std_lib_logger_filter", value)
629
+
630
+ @std_lib_logger_filter = value
631
+ end
632
+
590
633
  def environment=(environment)
591
634
  @environment = environment.to_s
592
635
  end
@@ -15,7 +15,8 @@ module Sentry
15
15
  # rate limits and client reports use the data_category rather than envelope item type
16
16
  def self.data_category(type)
17
17
  case type
18
- when "session", "attachment", "transaction", "profile", "span", "log" then type
18
+ when "session", "attachment", "transaction", "profile", "span", "trace_metric" then type
19
+ when "log" then "log_item"
19
20
  when "sessions" then "session"
20
21
  when "check_in" then "monitor"
21
22
  when "event" then "error"
data/lib/sentry/hub.rb CHANGED
@@ -227,6 +227,29 @@ module Sentry
227
227
  current_client.buffer_log_event(event, current_scope)
228
228
  end
229
229
 
230
+ # Captures a metric and sends it to Sentry
231
+ #
232
+ # @param name [String] the metric name
233
+ # @param type [Symbol] the metric type (:counter, :gauge, :distribution)
234
+ # @param value [Numeric] the metric value
235
+ # @param unit [String, nil] (optional) the metric unit
236
+ # @param attributes [Hash, nil] (optional) additional attributes for the metric
237
+ # @return [void]
238
+ def capture_metric(name:, type:, value:, unit: nil, attributes: nil)
239
+ return unless current_client&.configuration.enable_metrics
240
+
241
+ metric = MetricEvent.new(
242
+ name: name,
243
+ value: value,
244
+ type: type,
245
+ unit: unit,
246
+ attributes: attributes,
247
+ )
248
+
249
+ current_client.buffer_metric_event(metric, current_scope)
250
+ end
251
+
252
+
230
253
  def capture_event(event, **options, &block)
231
254
  check_argument_type!(event, Sentry::Event)
232
255
 
@@ -60,7 +60,7 @@ module Sentry
60
60
  end
61
61
  end
62
62
 
63
- stacktrace.frames.last.vars = locals
63
+ stacktrace.frames.last&.vars = locals
64
64
  end
65
65
 
66
66
  new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
@@ -1,134 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sentry/utils/telemetry_attributes"
4
+
3
5
  module Sentry
4
6
  # Event type that represents a log entry with its attributes
5
7
  #
6
8
  # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload
7
9
  class LogEvent
10
+ include Sentry::Utils::TelemetryAttributes
11
+
8
12
  TYPE = "log"
9
13
 
10
14
  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
- "sentry.origin" => :origin
34
- }
35
15
 
36
16
  PARAMETER_PREFIX = "sentry.message.parameter"
37
17
 
38
- USER_ATTRIBUTES = {
39
- "user.id" => :user_id,
40
- "user.name" => :user_username,
41
- "user.email" => :user_email
42
- }
43
-
44
18
  LEVELS = %i[trace debug info warn error fatal].freeze
45
19
 
46
- attr_accessor :level, :body, :template, :attributes, :user, :origin
47
-
48
- attr_reader :configuration, *(SERIALIZEABLE_ATTRIBUTES - %i[level body attributes])
49
-
50
- SERIALIZERS = %i[
51
- attributes
52
- body
53
- level
54
- parent_span_id
55
- sdk_name
56
- sdk_version
57
- template
58
- timestamp
59
- trace_id
60
- user_id
61
- user_username
62
- user_email
63
- ].map { |name| [name, :"serialize_#{name}"] }.to_h
64
-
65
- VALUE_TYPES = Hash.new("string").merge!({
66
- TrueClass => "boolean",
67
- FalseClass => "boolean",
68
- Integer => "integer",
69
- Float => "double"
70
- }).freeze
20
+ attr_accessor :level, :body, :template, :attributes, :origin, :trace_id, :span_id
21
+ attr_reader :timestamp
71
22
 
72
23
  TOKEN_REGEXP = /%\{(\w+)\}/
73
24
 
74
- def initialize(configuration: Sentry.configuration, **options)
75
- @configuration = configuration
25
+ def initialize(**options)
76
26
  @type = TYPE
77
- @server_name = configuration.server_name
78
- @environment = configuration.environment
79
- @release = configuration.release
80
27
  @timestamp = Sentry.utc_now
81
28
  @level = options.fetch(:level)
82
29
  @body = options[:body]
83
30
  @template = @body if is_template?
84
- @attributes = options[:attributes] || DEFAULT_ATTRIBUTES
85
- @user = options[:user] || {}
31
+ @attributes = options[:attributes] || {}
86
32
  @origin = options[:origin]
87
- @contexts = {}
33
+ @trace_id = nil
34
+ @span_id = nil
88
35
  end
89
36
 
90
37
  def to_h
91
- SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo|
92
- memo[name] = serialize(name)
93
- end
38
+ {
39
+ level: level.to_s,
40
+ timestamp: timestamp.to_f,
41
+ trace_id: @trace_id,
42
+ span_id: @span_id,
43
+ body: serialize_body,
44
+ attributes: serialize_attributes
45
+ }.compact
94
46
  end
95
47
 
96
48
  private
97
49
 
98
- def serialize(name)
99
- serializer = SERIALIZERS[name]
100
-
101
- if serializer
102
- __send__(serializer)
103
- else
104
- public_send(name)
105
- end
106
- end
107
-
108
- def serialize_level
109
- level.to_s
110
- end
111
-
112
- def serialize_sdk_name
113
- Sentry.sdk_meta["name"]
114
- end
115
-
116
- def serialize_sdk_version
117
- Sentry.sdk_meta["version"]
118
- end
119
-
120
- def serialize_timestamp
121
- timestamp.to_f
122
- end
123
-
124
- def serialize_trace_id
125
- contexts.dig(:trace, :trace_id)
126
- end
127
-
128
- def serialize_parent_span_id
129
- contexts.dig(:trace, :parent_span_id)
130
- end
131
-
132
50
  def serialize_body
133
51
  if parameters.empty?
134
52
  body
@@ -139,50 +57,14 @@ module Sentry
139
57
  end
140
58
  end
141
59
 
142
- def serialize_user_id
143
- user[:id]
144
- end
145
-
146
- def serialize_user_username
147
- user[:username]
148
- end
149
-
150
- def serialize_user_email
151
- user[:email]
152
- end
153
-
154
- def serialize_template
155
- template if has_parameters?
156
- end
157
-
158
60
  def serialize_attributes
159
- hash = {}
160
-
161
- attributes.each do |key, value|
162
- hash[key] = attribute_hash(value)
163
- end
164
-
165
- SENTRY_ATTRIBUTES.each do |key, name|
166
- if (value = serialize(name))
167
- hash[key] = attribute_hash(value)
168
- end
169
- end
170
-
171
- USER_ATTRIBUTES.each do |key, name|
172
- if (value = serialize(name))
173
- hash[key] = value
174
- end
175
- end
176
-
177
- hash
178
- end
179
-
180
- def attribute_hash(value)
181
- { value: value, type: value_type(value) }
61
+ populate_sentry_attributes!
62
+ @attributes.transform_values! { |v| attribute_hash(v) }
182
63
  end
183
64
 
184
- def value_type(value)
185
- VALUE_TYPES[value.class]
65
+ def populate_sentry_attributes!
66
+ @attributes["sentry.origin"] ||= @origin if @origin
67
+ @attributes["sentry.message.template"] ||= template if has_parameters?
186
68
  end
187
69
 
188
70
  def parameters
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sentry/threaded_periodic_worker"
3
+ require "sentry/telemetry_event_buffer"
4
4
 
5
5
  module Sentry
6
6
  # LogEventBuffer buffers log events and sends them to Sentry in a single envelope.
@@ -8,75 +8,21 @@ module Sentry
8
8
  # This is used internally by the `Sentry::Client`.
9
9
  #
10
10
  # @!visibility private
11
- class LogEventBuffer < ThreadedPeriodicWorker
12
- FLUSH_INTERVAL = 5 # seconds
11
+ class LogEventBuffer < TelemetryEventBuffer
13
12
  DEFAULT_MAX_EVENTS = 100
14
-
15
- # @!visibility private
16
- attr_reader :pending_events
13
+ MAX_EVENTS_BEFORE_DROP = 1000
17
14
 
18
15
  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
- def clear!
69
- @pending_events.clear
70
- end
71
-
72
- private
73
-
74
- def send_events
75
- @client.send_logs(@pending_events)
76
- rescue => e
77
- log_debug("[LogEventBuffer] Failed to send logs: #{e.message}")
78
- ensure
79
- clear!
16
+ super(
17
+ configuration,
18
+ client,
19
+ event_class: LogEvent,
20
+ max_items: configuration.max_log_events || DEFAULT_MAX_EVENTS,
21
+ max_items_before_drop: MAX_EVENTS_BEFORE_DROP,
22
+ envelope_type: "log",
23
+ envelope_content_type: "application/vnd.sentry.items.log+json",
24
+ before_send: configuration.before_send_log
25
+ )
80
26
  end
81
27
  end
82
28
  end
@@ -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,
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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/metric_event"
4
+
5
+ module Sentry
6
+ module Metrics
7
+ class << self
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
+ )
22
+ end
23
+
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
+ )
40
+ end
41
+
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
+ )
58
+ end
59
+ end
60
+ end
61
+ end
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) || event.is_a?(LogEvent)
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,10 +60,6 @@ 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
-
67
63
  if span
68
64
  event.contexts[:trace] ||= span.get_trace_context
69
65
 
@@ -89,6 +85,37 @@ module Sentry
89
85
  event
90
86
  end
91
87
 
88
+ # A leaner version of apply_to_event that applies to
89
+ # lightweight payloads like Logs and Metrics.
90
+ #
91
+ # Adds trace_id, span_id, user from the scope and default attributes from configuration.
92
+ #
93
+ # @param telemetry [MetricEvent, LogEvent] the telemetry event to apply scope context to
94
+ # @return [MetricEvent, LogEvent] the telemetry event with scope context applied
95
+ def apply_to_telemetry(telemetry)
96
+ # TODO-neel when new scope set_attribute api is added: add them here
97
+ trace_context = span ? span.get_trace_context : propagation_context.get_trace_context
98
+ telemetry.trace_id = trace_context[:trace_id]
99
+ telemetry.span_id = trace_context[:span_id]
100
+
101
+ configuration = Sentry.configuration
102
+ return telemetry unless configuration
103
+
104
+ telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"]
105
+ telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"]
106
+ telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment
107
+ telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
108
+ telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
109
+
110
+ if configuration.send_default_pii && !user.empty?
111
+ telemetry.attributes["user.id"] ||= user[:id] if user[:id]
112
+ telemetry.attributes["user.name"] ||= user[:username] if user[:username]
113
+ telemetry.attributes["user.email"] ||= user[:email] if user[:email]
114
+ end
115
+
116
+ telemetry
117
+ end
118
+
92
119
  # Adds the breadcrumb to the scope's breadcrumbs buffer.
93
120
  # @param breadcrumb [Breadcrumb]
94
121
  # @return [void]
@@ -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
@@ -37,6 +37,10 @@ module Sentry
37
37
  message = message.to_s.strip
38
38
 
39
39
  if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
40
+ if (filter = Sentry.configuration.std_lib_logger_filter) && !filter.call(self, message, method)
41
+ return result
42
+ end
43
+
40
44
  Sentry.logger.send(method, message, origin: ORIGIN)
41
45
  end
42
46
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/threaded_periodic_worker"
4
+ require "sentry/envelope"
5
+
6
+ module Sentry
7
+ # TelemetryEventBuffer is a base class for buffering telemetry events (logs, metrics, etc.)
8
+ # and sending them to Sentry in a single envelope.
9
+ #
10
+ # This is used internally by the `Sentry::Client`.
11
+ #
12
+ # @!visibility private
13
+ class TelemetryEventBuffer < ThreadedPeriodicWorker
14
+ FLUSH_INTERVAL = 5 # seconds
15
+
16
+ # @!visibility private
17
+ attr_reader :pending_items, :envelope_type, :data_category, :thread
18
+
19
+ def initialize(configuration, client, event_class:, max_items:, max_items_before_drop:, envelope_type:, envelope_content_type:, before_send:)
20
+ super(configuration.sdk_logger, FLUSH_INTERVAL)
21
+
22
+ @client = client
23
+ @dsn = configuration.dsn
24
+ @debug = configuration.debug
25
+ @event_class = event_class
26
+ @max_items = max_items
27
+ @max_items_before_drop = max_items_before_drop
28
+ @envelope_type = envelope_type
29
+ @data_category = Sentry::Envelope::Item.data_category(@envelope_type)
30
+ @envelope_content_type = envelope_content_type
31
+ @before_send = before_send
32
+
33
+ @pending_items = []
34
+ @mutex = Mutex.new
35
+
36
+ log_debug("[#{self.class}] Initialized buffer with max_items=#{@max_items}, flush_interval=#{FLUSH_INTERVAL}s")
37
+ end
38
+
39
+ def flush
40
+ @mutex.synchronize do
41
+ return if empty?
42
+
43
+ log_debug("[#{self.class}] flushing #{size} #{@event_class}")
44
+
45
+ send_items
46
+ end
47
+
48
+ self
49
+ end
50
+ alias_method :run, :flush
51
+
52
+ def add_item(item)
53
+ @mutex.synchronize do
54
+ return unless ensure_thread
55
+
56
+ if size >= @max_items_before_drop
57
+ log_debug("[#{self.class}] exceeded max capacity, dropping event")
58
+ @client.transport.record_lost_event(:queue_overflow, @data_category)
59
+ else
60
+ @pending_items << item
61
+ end
62
+
63
+ send_items if size >= @max_items
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ def empty?
70
+ @pending_items.empty?
71
+ end
72
+
73
+ def size
74
+ @pending_items.size
75
+ end
76
+
77
+ def clear!
78
+ @pending_items.clear
79
+ end
80
+
81
+ private
82
+
83
+ def send_items
84
+ envelope = Envelope.new(
85
+ event_id: Sentry::Utils.uuid,
86
+ sent_at: Sentry.utc_now.iso8601,
87
+ dsn: @dsn,
88
+ sdk: Sentry.sdk_meta
89
+ )
90
+
91
+ discarded_count = 0
92
+ envelope_items = []
93
+
94
+ if @before_send
95
+ @pending_items.each do |item|
96
+ processed_item = @before_send.call(item)
97
+
98
+ if processed_item
99
+ envelope_items << processed_item.to_h
100
+ else
101
+ discarded_count += 1
102
+ end
103
+ end
104
+ else
105
+ envelope_items = @pending_items.map(&:to_h)
106
+ end
107
+
108
+ unless discarded_count.zero?
109
+ @client.transport.record_lost_event(:before_send, @data_category, num: discarded_count)
110
+ end
111
+
112
+ return if envelope_items.empty?
113
+
114
+ envelope.add_item(
115
+ {
116
+ type: @envelope_type,
117
+ item_count: envelope_items.size,
118
+ content_type: @envelope_content_type
119
+ },
120
+ { items: envelope_items }
121
+ )
122
+
123
+ @client.send_envelope(envelope)
124
+ rescue => e
125
+ log_error("[#{self.class}] Failed to send #{@event_class}", e, debug: @debug)
126
+ ensure
127
+ clear!
128
+ end
129
+ end
130
+ end
@@ -102,6 +102,13 @@ module Sentry
102
102
  .flat_map { |item| item.payload[:items] }
103
103
  end
104
104
 
105
+ def sentry_metrics
106
+ sentry_envelopes
107
+ .flat_map(&:items)
108
+ .select { |item| item.headers[:type] == "trace_metric" }
109
+ .flat_map { |item| item.payload[:items] }
110
+ end
111
+
105
112
  # Returns the last captured event object.
106
113
  # @return [Event, nil]
107
114
  def last_sentry_event
@@ -6,26 +6,40 @@ module Sentry
6
6
  # @!visibility private
7
7
  def log_error(message, exception, debug: false)
8
8
  message = "#{message}: #{exception.message}"
9
- message += "\n#{exception.backtrace.join("\n")}" if debug
9
+ message += "\n#{exception.backtrace.join("\n")}" if debug && exception.backtrace
10
10
 
11
- sdk_logger&.error(LOGGER_PROGNAME) do
12
- message
13
- end
11
+ sdk_logger&.error(LOGGER_PROGNAME) { message }
12
+ rescue StandardError => e
13
+ log_to_stderr(e, message)
14
14
  end
15
15
 
16
16
  # @!visibility private
17
17
  def log_debug(message)
18
18
  sdk_logger&.debug(LOGGER_PROGNAME) { message }
19
+ rescue StandardError => e
20
+ log_to_stderr(e, message)
19
21
  end
20
22
 
21
23
  # @!visibility private
22
24
  def log_warn(message)
23
25
  sdk_logger&.warn(LOGGER_PROGNAME) { message }
26
+ rescue StandardError => e
27
+ log_to_stderr(e, message)
24
28
  end
25
29
 
26
30
  # @!visibility private
27
31
  def sdk_logger
28
32
  @sdk_logger ||= Sentry.sdk_logger
29
33
  end
34
+
35
+ # @!visibility private
36
+ def log_to_stderr(error, message)
37
+ error_msg = "Sentry SDK logging failed (#{error.class}: #{error.message}): #{message}".scrub(%q(<?>))
38
+ error_msg += "\n#{error.backtrace.map { |line| line.scrub(%q(<?>)) }.join("\n")}" if error.backtrace
39
+
40
+ $stderr.puts(error_msg)
41
+ rescue StandardError
42
+ # swallow everything – logging must never crash the app
43
+ end
30
44
  end
31
45
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Sentry
6
+ module Utils
7
+ module TelemetryAttributes
8
+ private
9
+
10
+ def attribute_hash(value)
11
+ case value
12
+ when String
13
+ { value: value, type: "string" }
14
+ when TrueClass, FalseClass
15
+ { value: value, type: "boolean" }
16
+ when Integer
17
+ { value: value, type: "integer" }
18
+ when Float
19
+ { value: value, type: "double" }
20
+ else
21
+ begin
22
+ { value: JSON.generate(value), type: "string" }
23
+ rescue
24
+ { value: value, type: "string" }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "6.2.0"
4
+ VERSION = "6.3.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -27,6 +27,7 @@ require "sentry/session_flusher"
27
27
  require "sentry/backpressure_monitor"
28
28
  require "sentry/cron/monitor_check_ins"
29
29
  require "sentry/vernier/profiler"
30
+ require "sentry/metrics"
30
31
 
31
32
  [
32
33
  "sentry/rake",
@@ -627,6 +628,24 @@ module Sentry
627
628
  @logger ||= configuration.structured_logging.logger_class.new(configuration)
628
629
  end
629
630
 
631
+ # Returns the metrics API for capturing custom metrics.
632
+ #
633
+ # @example Enable metrics
634
+ # Sentry.init do |config|
635
+ # config.dsn = "YOUR_DSN"
636
+ # config.enable_metrics = true
637
+ # end
638
+ #
639
+ # @example Usage
640
+ # Sentry.metrics.count("button.click", 1, attributes: { button_id: "submit" })
641
+ # Sentry.metrics.distribution("response.time", 120.5, unit: "millisecond")
642
+ # Sentry.metrics.gauge("cpu.usage", 75.2, unit: "percent")
643
+ #
644
+ # @return [Metrics] The metrics API
645
+ def metrics
646
+ Metrics
647
+ end
648
+
630
649
  ##### Helpers #####
631
650
 
632
651
  # @!visibility private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.0
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
@@ -106,6 +106,9 @@ files:
106
106
  - lib/sentry/log_event.rb
107
107
  - lib/sentry/log_event_buffer.rb
108
108
  - lib/sentry/logger.rb
109
+ - lib/sentry/metric_event.rb
110
+ - lib/sentry/metric_event_buffer.rb
111
+ - lib/sentry/metrics.rb
109
112
  - lib/sentry/net/http.rb
110
113
  - lib/sentry/profiler.rb
111
114
  - lib/sentry/profiler/helpers.rb
@@ -118,11 +121,13 @@ files:
118
121
  - lib/sentry/release_detector.rb
119
122
  - lib/sentry/rspec.rb
120
123
  - lib/sentry/scope.rb
124
+ - lib/sentry/sequel.rb
121
125
  - lib/sentry/session.rb
122
126
  - lib/sentry/session_flusher.rb
123
127
  - lib/sentry/span.rb
124
128
  - lib/sentry/std_lib_logger.rb
125
129
  - lib/sentry/structured_logger.rb
130
+ - lib/sentry/telemetry_event_buffer.rb
126
131
  - lib/sentry/test_helper.rb
127
132
  - lib/sentry/threaded_periodic_worker.rb
128
133
  - lib/sentry/transaction.rb
@@ -143,21 +148,22 @@ files:
143
148
  - lib/sentry/utils/real_ip.rb
144
149
  - lib/sentry/utils/request_id.rb
145
150
  - lib/sentry/utils/sample_rand.rb
151
+ - lib/sentry/utils/telemetry_attributes.rb
146
152
  - lib/sentry/utils/uuid.rb
147
153
  - lib/sentry/vernier/output.rb
148
154
  - lib/sentry/vernier/profiler.rb
149
155
  - lib/sentry/version.rb
150
156
  - sentry-ruby-core.gemspec
151
157
  - sentry-ruby.gemspec
152
- homepage: https://github.com/getsentry/sentry-ruby/tree/6.2.0/sentry-ruby
158
+ homepage: https://github.com/getsentry/sentry-ruby/tree/6.3.0/sentry-ruby
153
159
  licenses:
154
160
  - MIT
155
161
  metadata:
156
- homepage_uri: https://github.com/getsentry/sentry-ruby/tree/6.2.0/sentry-ruby
157
- source_code_uri: https://github.com/getsentry/sentry-ruby/tree/6.2.0/sentry-ruby
158
- changelog_uri: https://github.com/getsentry/sentry-ruby/blob/6.2.0/CHANGELOG.md
162
+ homepage_uri: https://github.com/getsentry/sentry-ruby/tree/6.3.0/sentry-ruby
163
+ source_code_uri: https://github.com/getsentry/sentry-ruby/tree/6.3.0/sentry-ruby
164
+ changelog_uri: https://github.com/getsentry/sentry-ruby/blob/6.3.0/CHANGELOG.md
159
165
  bug_tracker_uri: https://github.com/getsentry/sentry-ruby/issues
160
- documentation_uri: http://www.rubydoc.info/gems/sentry-ruby/6.2.0
166
+ documentation_uri: http://www.rubydoc.info/gems/sentry-ruby/6.3.0
161
167
  rdoc_options: []
162
168
  require_paths:
163
169
  - lib