sentry-ruby 5.22.4 → 5.26.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.
@@ -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
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,6 +2,7 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "sentry/baggage"
5
+ require "sentry/utils/uuid"
5
6
 
6
7
  module Sentry
7
8
  class PropagationContext
@@ -66,8 +67,8 @@ module Sentry
66
67
  end
67
68
  end
68
69
 
69
- @trace_id ||= SecureRandom.uuid.delete("-")
70
- @span_id = SecureRandom.uuid.delete("-").slice(0, 16)
70
+ @trace_id ||= Utils.uuid
71
+ @span_id = Utils.uuid.slice(0, 16)
71
72
  end
72
73
 
73
74
  # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
@@ -124,8 +125,7 @@ module Sentry
124
125
  "trace_id" => trace_id,
125
126
  "environment" => configuration.environment,
126
127
  "release" => configuration.release,
127
- "public_key" => configuration.dsn&.public_key,
128
- "user_segment" => @scope.user && @scope.user["segment"]
128
+ "public_key" => configuration.dsn&.public_key
129
129
  }
130
130
 
131
131
  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
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # The StructuredLogger class implements Sentry's SDK telemetry logs protocol.
5
+ # It provides methods for logging messages at different severity levels and
6
+ # sending them to Sentry with structured data.
7
+ #
8
+ # This class follows the Sentry Logs Protocol as defined in:
9
+ # https://develop.sentry.dev/sdk/telemetry/logs/
10
+ #
11
+ # @example Basic usage
12
+ # Sentry.logger.info("User logged in", user_id: 123)
13
+ #
14
+ # @example With structured data
15
+ # Sentry.logger.warn("API request failed",
16
+ # status_code: 404,
17
+ # endpoint: "/api/users",
18
+ # request_id: "abc-123"
19
+ # )
20
+ #
21
+ # @example With a message template
22
+ # # Using positional parameters
23
+ # Sentry.logger.info("User %s logged in", ["Jane Doe"])
24
+ #
25
+ # # Using hash parameters
26
+ # Sentry.logger.info("User %{name} logged in", name: "Jane Doe")
27
+ #
28
+ # # Using hash parameters and extra attributes
29
+ # Sentry.logger.info("User %{name} logged in", name: "Jane Doe", user_id: 312)
30
+ #
31
+ # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol
32
+ class StructuredLogger
33
+ # Severity number mapping for log levels according to the Sentry Logs Protocol
34
+ # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number
35
+ LEVELS = {
36
+ trace: 1,
37
+ debug: 5,
38
+ info: 9,
39
+ warn: 13,
40
+ error: 17,
41
+ fatal: 21
42
+ }.freeze
43
+
44
+ # @return [Configuration] The Sentry configuration
45
+ # @!visibility private
46
+ attr_reader :config
47
+
48
+ # Initializes a new StructuredLogger instance
49
+ #
50
+ # @param config [Configuration] The Sentry configuration
51
+ def initialize(config)
52
+ @config = config
53
+ end
54
+
55
+ # Logs a message at TRACE level
56
+ #
57
+ # @param message [String] The log message
58
+ # @param parameters [Array] Array of values to replace template parameters in the message
59
+ # @param attributes [Hash] Additional attributes to include with the log
60
+ #
61
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
62
+ def trace(message, parameters = [], **attributes)
63
+ log(__method__, message, parameters: parameters, **attributes)
64
+ end
65
+
66
+ # Logs a message at DEBUG level
67
+ #
68
+ # @param message [String] The log message
69
+ # @param parameters [Array] Array of values to replace template parameters in the message
70
+ # @param attributes [Hash] Additional attributes to include with the log
71
+ #
72
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
73
+ def debug(message, parameters = [], **attributes)
74
+ log(__method__, message, parameters: parameters, **attributes)
75
+ end
76
+
77
+ # Logs a message at INFO level
78
+ #
79
+ # @param message [String] The log message
80
+ # @param parameters [Array] Array of values to replace template parameters in the message
81
+ # @param attributes [Hash] Additional attributes to include with the log
82
+ #
83
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
84
+ def info(message, parameters = [], **attributes)
85
+ log(__method__, message, parameters: parameters, **attributes)
86
+ end
87
+
88
+ # Logs a message at WARN level
89
+ #
90
+ # @param message [String] The log message
91
+ # @param parameters [Array] Array of values to replace template parameters in the message
92
+ # @param attributes [Hash] Additional attributes to include with the log
93
+ #
94
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
95
+ def warn(message, parameters = [], **attributes)
96
+ log(__method__, message, parameters: parameters, **attributes)
97
+ end
98
+
99
+ # Logs a message at ERROR level
100
+ #
101
+ # @param message [String] The log message
102
+ # @param parameters [Array] Array of values to replace template parameters in the message
103
+ # @param attributes [Hash] Additional attributes to include with the log
104
+ #
105
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
106
+ def error(message, parameters = [], **attributes)
107
+ log(__method__, message, parameters: parameters, **attributes)
108
+ end
109
+
110
+ # Logs a message at FATAL level
111
+ #
112
+ # @param message [String] The log message
113
+ # @param parameters [Array] Array of values to replace template parameters in the message
114
+ # @param attributes [Hash] Additional attributes to include with the log
115
+ #
116
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
117
+ def fatal(message, parameters = [], **attributes)
118
+ log(__method__, message, parameters: parameters, **attributes)
119
+ end
120
+
121
+ # Logs a message at the specified level
122
+ #
123
+ # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal)
124
+ # @param message [String] The log message
125
+ # @param parameters [Array, Hash] Array or Hash of values to replace template parameters in the message
126
+ # @param attributes [Hash] Additional attributes to include with the log
127
+ #
128
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
129
+ def log(level, message, parameters:, **attributes)
130
+ case parameters
131
+ when Array then
132
+ Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes)
133
+ else
134
+ Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -72,6 +72,13 @@ module Sentry
72
72
  sentry_transport.envelopes
73
73
  end
74
74
 
75
+ def sentry_logs
76
+ sentry_envelopes
77
+ .flat_map(&:items)
78
+ .select { |item| item.headers[:type] == "log" }
79
+ .flat_map { |item| item.payload[:items] }
80
+ end
81
+
75
82
  # Returns the last captured event object.
76
83
  # @return [Event, nil]
77
84
  def last_sentry_event
@@ -83,5 +90,18 @@ module Sentry
83
90
  def extract_sentry_exceptions(event)
84
91
  event&.exception&.values || []
85
92
  end
93
+
94
+ def reset_sentry_globals!
95
+ Sentry::MUTEX.synchronize do
96
+ # Don't check initialized? because sometimes we stub it in tests
97
+ if Sentry.instance_variable_defined?(:@main_hub)
98
+ Sentry::GLOBALS.each do |var|
99
+ Sentry.instance_variable_set(:"@#{var}", nil)
100
+ end
101
+
102
+ Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, nil)
103
+ end
104
+ end
105
+ end
86
106
  end
87
107
  end
@@ -4,11 +4,11 @@ module Sentry
4
4
  class ThreadedPeriodicWorker
5
5
  include LoggingHelper
6
6
 
7
- def initialize(logger, internal)
7
+ def initialize(sdk_logger, interval)
8
8
  @thread = nil
9
9
  @exited = false
10
- @interval = internal
11
- @logger = logger
10
+ @interval = interval
11
+ @sdk_logger = sdk_logger
12
12
  end
13
13
 
14
14
  def ensure_thread