airbrake-ruby 4.6.0 → 6.2.2

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 (107) hide show
  1. checksums.yaml +5 -5
  2. data/lib/airbrake-ruby/async_sender.rb +24 -109
  3. data/lib/airbrake-ruby/backlog.rb +123 -0
  4. data/lib/airbrake-ruby/backtrace.rb +8 -7
  5. data/lib/airbrake-ruby/benchmark.rb +1 -1
  6. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  7. data/lib/airbrake-ruby/config/processor.rb +77 -0
  8. data/lib/airbrake-ruby/config/validator.rb +9 -3
  9. data/lib/airbrake-ruby/config.rb +78 -19
  10. data/lib/airbrake-ruby/context.rb +51 -0
  11. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  12. data/lib/airbrake-ruby/file_cache.rb +6 -0
  13. data/lib/airbrake-ruby/filter_chain.rb +18 -1
  14. data/lib/airbrake-ruby/filters/context_filter.rb +4 -5
  15. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  16. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +3 -3
  17. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  18. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +7 -7
  19. data/lib/airbrake-ruby/filters/git_repository_filter.rb +11 -2
  20. data/lib/airbrake-ruby/filters/git_revision_filter.rb +3 -1
  21. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  22. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  23. data/lib/airbrake-ruby/filters/keys_filter.rb +41 -22
  24. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  25. data/lib/airbrake-ruby/filters/sql_filter.rb +35 -11
  26. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  27. data/lib/airbrake-ruby/filters/thread_filter.rb +6 -5
  28. data/lib/airbrake-ruby/grouppable.rb +12 -0
  29. data/lib/airbrake-ruby/ignorable.rb +1 -2
  30. data/lib/airbrake-ruby/inspectable.rb +2 -2
  31. data/lib/airbrake-ruby/loggable.rb +1 -1
  32. data/lib/airbrake-ruby/mergeable.rb +12 -0
  33. data/lib/airbrake-ruby/monotonic_time.rb +6 -1
  34. data/lib/airbrake-ruby/nested_exception.rb +22 -1
  35. data/lib/airbrake-ruby/notice.rb +12 -17
  36. data/lib/airbrake-ruby/notice_notifier.rb +15 -7
  37. data/lib/airbrake-ruby/performance_breakdown.rb +16 -9
  38. data/lib/airbrake-ruby/performance_notifier.rb +106 -54
  39. data/lib/airbrake-ruby/promise.rb +1 -0
  40. data/lib/airbrake-ruby/query.rb +20 -14
  41. data/lib/airbrake-ruby/queue.rb +65 -0
  42. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  43. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  44. data/lib/airbrake-ruby/remote_settings.rb +128 -0
  45. data/lib/airbrake-ruby/request.rb +14 -11
  46. data/lib/airbrake-ruby/response.rb +56 -5
  47. data/lib/airbrake-ruby/stat.rb +27 -34
  48. data/lib/airbrake-ruby/sync_sender.rb +43 -11
  49. data/lib/airbrake-ruby/tdigest.rb +52 -66
  50. data/lib/airbrake-ruby/thread_pool.rb +139 -0
  51. data/lib/airbrake-ruby/time_truncate.rb +2 -2
  52. data/lib/airbrake-ruby/timed_trace.rb +1 -3
  53. data/lib/airbrake-ruby/truncator.rb +10 -4
  54. data/lib/airbrake-ruby/version.rb +11 -1
  55. data/lib/airbrake-ruby.rb +157 -78
  56. metadata +122 -116
  57. data/spec/airbrake_spec.rb +0 -324
  58. data/spec/async_sender_spec.rb +0 -155
  59. data/spec/backtrace_spec.rb +0 -427
  60. data/spec/benchmark_spec.rb +0 -33
  61. data/spec/code_hunk_spec.rb +0 -115
  62. data/spec/config/validator_spec.rb +0 -184
  63. data/spec/config_spec.rb +0 -154
  64. data/spec/deploy_notifier_spec.rb +0 -48
  65. data/spec/file_cache.rb +0 -36
  66. data/spec/filter_chain_spec.rb +0 -92
  67. data/spec/filters/context_filter_spec.rb +0 -23
  68. data/spec/filters/dependency_filter_spec.rb +0 -12
  69. data/spec/filters/exception_attributes_filter_spec.rb +0 -50
  70. data/spec/filters/gem_root_filter_spec.rb +0 -41
  71. data/spec/filters/git_last_checkout_filter_spec.rb +0 -46
  72. data/spec/filters/git_repository_filter.rb +0 -61
  73. data/spec/filters/git_revision_filter_spec.rb +0 -126
  74. data/spec/filters/keys_blacklist_spec.rb +0 -225
  75. data/spec/filters/keys_whitelist_spec.rb +0 -194
  76. data/spec/filters/root_directory_filter_spec.rb +0 -39
  77. data/spec/filters/sql_filter_spec.rb +0 -219
  78. data/spec/filters/system_exit_filter_spec.rb +0 -14
  79. data/spec/filters/thread_filter_spec.rb +0 -277
  80. data/spec/fixtures/notroot.txt +0 -7
  81. data/spec/fixtures/project_root/code.rb +0 -221
  82. data/spec/fixtures/project_root/empty_file.rb +0 -0
  83. data/spec/fixtures/project_root/long_line.txt +0 -1
  84. data/spec/fixtures/project_root/short_file.rb +0 -3
  85. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +0 -5
  86. data/spec/helpers.rb +0 -9
  87. data/spec/ignorable_spec.rb +0 -14
  88. data/spec/inspectable_spec.rb +0 -45
  89. data/spec/monotonic_time_spec.rb +0 -12
  90. data/spec/nested_exception_spec.rb +0 -73
  91. data/spec/notice_notifier_spec/options_spec.rb +0 -259
  92. data/spec/notice_notifier_spec.rb +0 -356
  93. data/spec/notice_spec.rb +0 -296
  94. data/spec/performance_breakdown_spec.rb +0 -12
  95. data/spec/performance_notifier_spec.rb +0 -435
  96. data/spec/promise_spec.rb +0 -197
  97. data/spec/query_spec.rb +0 -11
  98. data/spec/request_spec.rb +0 -11
  99. data/spec/response_spec.rb +0 -88
  100. data/spec/spec_helper.rb +0 -100
  101. data/spec/stashable_spec.rb +0 -23
  102. data/spec/stat_spec.rb +0 -47
  103. data/spec/sync_sender_spec.rb +0 -133
  104. data/spec/tdigest_spec.rb +0 -230
  105. data/spec/time_truncate_spec.rb +0 -13
  106. data/spec/timed_trace_spec.rb +0 -125
  107. data/spec/truncator_spec.rb +0 -238
@@ -21,7 +21,7 @@ module Airbrake
21
21
  project_id: @config.project_id,
22
22
  project_key: @config.project_key,
23
23
  host: @config.host,
24
- filter_chain: @filter_chain.inspect
24
+ filter_chain: @filter_chain.inspect,
25
25
  )
26
26
  end
27
27
 
@@ -30,7 +30,7 @@ module Airbrake
30
30
  q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
31
31
  q.text(
32
32
  "project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
33
- "host=\"#{@config.host}\" filter_chain="
33
+ "host=\"#{@config.host}\" filter_chain=",
34
34
  )
35
35
  q.pp(@filter_chain)
36
36
  q.text('>')
@@ -22,7 +22,7 @@ module Airbrake
22
22
 
23
23
  # @return [Logger]
24
24
  def instance
25
- @instance ||= ::Logger.new(File::NULL)
25
+ @instance ||= ::Logger.new(File::NULL).tap { |l| l.level = ::Logger::WARN }
26
26
  end
27
27
  end
28
28
 
@@ -0,0 +1,12 @@
1
+ module Airbrake
2
+ # Mergeable adds the `#merge` method, so that we don't need to define it in
3
+ # all of performance models every time we add a model.
4
+ #
5
+ # @since v4.9.0
6
+ # @api private
7
+ module Mergeable
8
+ def merge(_other)
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -16,6 +16,11 @@ module Airbrake
16
16
  time_in_nanoseconds / (10.0**6)
17
17
  end
18
18
 
19
+ # @return [Integer] current monotonic time in seconds
20
+ def time_in_s
21
+ time_in_nanoseconds / (10.0**9)
22
+ end
23
+
19
24
  private
20
25
 
21
26
  if defined?(Process::CLOCK_MONOTONIC)
@@ -34,7 +39,7 @@ module Airbrake
34
39
 
35
40
  def time_in_nanoseconds
36
41
  time = Time.now
37
- time.to_i * (10**9) + time.nsec
42
+ (time.to_i * (10**9)) + time.nsec
38
43
  end
39
44
 
40
45
  end
@@ -9,6 +9,18 @@ module Airbrake
9
9
  # can unwrap. Exceptions that have a longer cause chain will be ignored
10
10
  MAX_NESTED_EXCEPTIONS = 3
11
11
 
12
+ # On Ruby 3.1+, the error highlighting gem can produce messages that can
13
+ # span multiple lines. We don't display multiline error messages in the
14
+ # title of the notice in the Airbrake dashboard. Therefore, we want to strip
15
+ # out the higlighting part so that the errors look consistent. The full
16
+ # message with the exception will be attached to the notice body.
17
+ #
18
+ # @return [String]
19
+ RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n".freeze
20
+
21
+ # @return [Hash] the options for +String#encode+
22
+ ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
23
+
12
24
  def initialize(exception)
13
25
  @exception = exception
14
26
  end
@@ -16,7 +28,7 @@ module Airbrake
16
28
  def as_json
17
29
  unwind_exceptions.map do |exception|
18
30
  { type: exception.class.name,
19
- message: exception.message,
31
+ message: message(exception),
20
32
  backtrace: Backtrace.parse(exception) }
21
33
  end
22
34
  end
@@ -34,5 +46,14 @@ module Airbrake
34
46
 
35
47
  exception_list
36
48
  end
49
+
50
+ def message(exception)
51
+ return unless (msg = exception.message)
52
+
53
+ msg
54
+ .encode(Encoding::UTF_8, **ENCODING_OPTIONS)
55
+ .split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
56
+ .first
57
+ end
37
58
  end
38
59
  end
@@ -4,19 +4,12 @@ module Airbrake
4
4
  #
5
5
  # @since v1.0.0
6
6
  class Notice
7
- # @return [Hash{Symbol=>String}] the information about the notifier library
8
- NOTIFIER = {
9
- name: 'airbrake-ruby'.freeze,
10
- version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
- url: 'https://github.com/airbrake/airbrake-ruby'.freeze
12
- }.freeze
13
-
14
7
  # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
8
  # Context tab in the dashboard
16
9
  CONTEXT = {
17
10
  os: RUBY_PLATFORM,
18
11
  language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
- notifier: NOTIFIER
12
+ notifier: Airbrake::NOTIFIER_INFO,
20
13
  }.freeze
21
14
 
22
15
  # @return [Integer] the maxium size of the JSON payload in bytes
@@ -32,7 +25,7 @@ module Airbrake
32
25
  IOError,
33
26
  NotImplementedError,
34
27
  JSON::GeneratorError,
35
- Encoding::UndefinedConversionError
28
+ Encoding::UndefinedConversionError,
36
29
  ].freeze
37
30
 
38
31
  # @return [Array<Symbol>] the list of keys that can be be overwritten with
@@ -56,16 +49,17 @@ module Airbrake
56
49
  # @api private
57
50
  def initialize(exception, params = {})
58
51
  @config = Airbrake::Config.instance
52
+ @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
53
+
59
54
  @payload = {
60
55
  errors: NestedException.new(exception).as_json,
61
- context: context,
56
+ context: context(exception),
62
57
  environment: {
63
- program_name: $PROGRAM_NAME
58
+ program_name: $PROGRAM_NAME,
64
59
  },
65
60
  session: {},
66
- params: params
61
+ params: params,
67
62
  }
68
- @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
69
63
 
70
64
  stash[:exception] = exception
71
65
  end
@@ -76,7 +70,7 @@ module Airbrake
76
70
  #
77
71
  # @return [Hash{String=>String}, nil]
78
72
  # @api private
79
- def to_json
73
+ def to_json(*_args)
80
74
  loop do
81
75
  begin
82
76
  json = @payload.to_json
@@ -125,7 +119,7 @@ module Airbrake
125
119
 
126
120
  private
127
121
 
128
- def context
122
+ def context(exception)
129
123
  {
130
124
  version: @config.app_version,
131
125
  versions: @config.versions,
@@ -138,7 +132,8 @@ module Airbrake
138
132
  # Make sure we always send hostname.
139
133
  hostname: HOSTNAME,
140
134
 
141
- severity: DEFAULT_SEVERITY
135
+ severity: DEFAULT_SEVERITY,
136
+ error_message: @truncator.truncate(exception.message),
142
137
  }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
143
138
  end
144
139
 
@@ -152,7 +147,7 @@ module Airbrake
152
147
  logger.error(
153
148
  "#{LOG_LABEL} truncation failed. File an issue at " \
154
149
  "https://github.com/airbrake/airbrake-ruby " \
155
- "and attach the following payload: #{@payload}"
150
+ "and attach the following payload: #{@payload}",
156
151
  )
157
152
  end
158
153
 
@@ -9,7 +9,7 @@ module Airbrake
9
9
  # @return [Array<Class>] filters to be executed first
10
10
  DEFAULT_FILTERS = [
11
11
  Airbrake::Filters::SystemExitFilter,
12
- Airbrake::Filters::GemRootFilter
12
+ Airbrake::Filters::GemRootFilter,
13
13
 
14
14
  # Optional filters (must be included by users):
15
15
  # Airbrake::Filters::ThreadFilter
@@ -20,14 +20,13 @@ module Airbrake
20
20
 
21
21
  def initialize
22
22
  @config = Airbrake::Config.instance
23
- @context = {}
24
23
  @filter_chain = FilterChain.new
25
- @async_sender = AsyncSender.new
24
+ @async_sender = AsyncSender.new(:post, self.class.name)
26
25
  @sync_sender = SyncSender.new
27
26
 
28
27
  DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
29
28
 
30
- add_filter(Airbrake::Filters::ContextFilter.new(@context))
29
+ add_filter(Airbrake::Filters::ContextFilter.new)
31
30
  add_filter(Airbrake::Filters::ExceptionAttributesFilter.new)
32
31
  end
33
32
 
@@ -55,7 +54,8 @@ module Airbrake
55
54
  def build_notice(exception, params = {})
56
55
  if @async_sender.closed?
57
56
  raise Airbrake::Error,
58
- "attempted to build #{exception} with closed Airbrake instance"
57
+ "Airbrake is closed; can't build exception: " \
58
+ "#{exception.class}: #{exception}"
59
59
  end
60
60
 
61
61
  if exception.is_a?(Airbrake::Notice)
@@ -68,6 +68,7 @@ module Airbrake
68
68
 
69
69
  # @see Airbrake.close
70
70
  def close
71
+ @sync_sender.close
71
72
  @async_sender.close
72
73
  end
73
74
 
@@ -78,7 +79,13 @@ module Airbrake
78
79
 
79
80
  # @see Airbrake.merge_context
80
81
  def merge_context(context)
81
- @context.merge!(context)
82
+ Airbrake::Context.current.merge!(context)
83
+ end
84
+
85
+ # @return [Boolean]
86
+ # @since v4.14.0
87
+ def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
88
+ @filter_chain.includes?(filter_class)
82
89
  end
83
90
 
84
91
  private
@@ -116,7 +123,7 @@ module Airbrake
116
123
 
117
124
  logger.warn(
118
125
  "#{LOG_LABEL} falling back to sync delivery because there are no " \
119
- "running async workers"
126
+ "running async workers",
120
127
  )
121
128
  @sync_sender
122
129
  end
@@ -128,6 +135,7 @@ module Airbrake
128
135
  # If true, then it's likely an internal library error. In this case return
129
136
  # at least some backtrace to simplify debugging.
130
137
  return caller_copy if clean_bt.empty?
138
+
131
139
  clean_bt
132
140
  end
133
141
  end
@@ -5,23 +5,30 @@ module Airbrake
5
5
  # @see Airbrake.notify_breakdown
6
6
  # @api public
7
7
  # @since v4.2.0
8
- # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
9
- PerformanceBreakdown = Struct.new(
10
- :method, :route, :response_type, :groups, :start_time, :end_time
11
- ) do
8
+ # rubocop:disable Metrics/ParameterLists
9
+ class PerformanceBreakdown
12
10
  include HashKeyable
13
11
  include Ignorable
14
12
  include Stashable
13
+ include Mergeable
14
+
15
+ attr_accessor :method, :route, :response_type, :groups, :timing, :time
15
16
 
16
17
  def initialize(
17
18
  method:,
18
19
  route:,
19
20
  response_type:,
20
21
  groups:,
21
- start_time:,
22
- end_time: Time.now
22
+ timing: nil,
23
+ time: Time.now
23
24
  )
24
- super(method, route, response_type, groups, start_time, end_time)
25
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
26
+ @method = method
27
+ @route = route
28
+ @response_type = response_type
29
+ @groups = groups
30
+ @timing = timing
31
+ @time = time
25
32
  end
26
33
 
27
34
  def destination
@@ -37,9 +44,9 @@ module Airbrake
37
44
  'method' => method,
38
45
  'route' => route,
39
46
  'responseType' => response_type,
40
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
47
+ 'time' => @time_utc,
41
48
  }.delete_if { |_key, val| val.nil? }
42
49
  end
43
50
  end
44
- # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
51
+ # rubocop:enable Metrics/ParameterLists
45
52
  end
@@ -1,9 +1,10 @@
1
1
  module Airbrake
2
- # QueryNotifier aggregates information about SQL queries and periodically sends
3
- # collected data to Airbrake.
2
+ # PerformanceNotifier aggregates performance data and periodically sends it to
3
+ # Airbrake.
4
4
  #
5
5
  # @api public
6
6
  # @since v3.2.0
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class PerformanceNotifier
8
9
  include Inspectable
9
10
  include Loggable
@@ -11,32 +12,29 @@ module Airbrake
11
12
  def initialize
12
13
  @config = Airbrake::Config.instance
13
14
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
- @sender = SyncSender.new(:put)
15
- @payload = {}
15
+ @async_sender = AsyncSender.new(:put, self.class.name)
16
+ @sync_sender = SyncSender.new(:put)
16
17
  @schedule_flush = nil
17
- @mutex = Mutex.new
18
18
  @filter_chain = FilterChain.new
19
+
20
+ @payload = {}.extend(MonitorMixin)
21
+ @has_payload = @payload.new_cond
19
22
  end
20
23
 
21
- # @param [Hash] resource
24
+ # @param [Hash] metric
22
25
  # @see Airbrake.notify_query
23
26
  # @see Airbrake.notify_request
24
- def notify(resource)
25
- promise = @config.check_configuration
26
- return promise if promise.rejected?
27
-
28
- promise = @config.check_performance_options(resource)
29
- return promise if promise.rejected?
30
-
31
- @filter_chain.refine(resource)
32
- return if resource.ignored?
33
-
34
- @mutex.synchronize do
35
- update_payload(resource)
36
- @flush_period > 0 ? schedule_flush(promise) : send(@payload, promise)
27
+ def notify(metric)
28
+ @payload.synchronize do
29
+ send_metric(metric, sync: false)
37
30
  end
31
+ end
38
32
 
39
- promise
33
+ # @param [Hash] metric
34
+ # @since v4.10.0
35
+ # @see Airbrake.notify_queue_sync
36
+ def notify_sync(metric)
37
+ send_metric(metric, sync: true).value
40
38
  end
41
39
 
42
40
  # @see Airbrake.add_performance_filter
@@ -49,77 +47,131 @@ module Airbrake
49
47
  @filter_chain.delete_filter(filter_class)
50
48
  end
51
49
 
50
+ def close
51
+ @payload.synchronize do
52
+ @schedule_flush.kill if @schedule_flush
53
+ @sync_sender.close
54
+ @async_sender.close
55
+ end
56
+ end
57
+
52
58
  private
53
59
 
54
- def update_payload(resource)
55
- @payload[resource] ||= { total: Airbrake::Stat.new }
56
- @payload[resource][:total].increment(resource.start_time, resource.end_time)
60
+ def schedule_flush
61
+ @schedule_flush ||= Thread.new do
62
+ loop do
63
+ @payload.synchronize do
64
+ @last_flush_time ||= MonotonicTime.time_in_s
65
+
66
+ while (MonotonicTime.time_in_s - @last_flush_time) < @flush_period
67
+ @has_payload.wait(@flush_period)
68
+ end
69
+
70
+ if @payload.none?
71
+ @last_flush_time = nil
72
+ next
73
+ end
57
74
 
58
- resource.groups.each do |name, ms|
59
- @payload[resource][name] ||= Airbrake::Stat.new
60
- @payload[resource][name].increment_ms(ms)
75
+ send(@async_sender, @payload, Airbrake::Promise.new)
76
+ @payload.clear
77
+ end
78
+ end
61
79
  end
62
80
  end
63
81
 
64
- def schedule_flush(promise)
65
- @schedule_flush ||= Thread.new do
66
- sleep(@flush_period)
82
+ def send_metric(metric, sync:)
83
+ promise = check_configuration(metric)
84
+ return promise if promise.rejected?
67
85
 
68
- payload = nil
69
- @mutex.synchronize do
70
- payload = @payload
71
- @payload = {}
72
- @schedule_flush = nil
73
- end
86
+ @filter_chain.refine(metric)
87
+ if metric.ignored?
88
+ return Promise.new.reject("#{metric.class} was ignored by a filter")
89
+ end
74
90
 
75
- send(payload, promise)
91
+ update_payload(metric)
92
+ if sync || @flush_period == 0
93
+ send(@sync_sender, @payload, promise)
94
+ else
95
+ @has_payload.signal
96
+ schedule_flush
76
97
  end
77
98
  end
78
99
 
79
- def send(payload, promise)
80
- signature = "#{self.class.name}##{__method__}"
81
- raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
100
+ def update_payload(metric)
101
+ if (total_stat = @payload[metric])
102
+ @payload.key(total_stat).merge(metric)
103
+ else
104
+ @payload[metric] = { total: Airbrake::Stat.new }
105
+ end
106
+
107
+ @payload[metric][:total].increment_ms(metric.timing)
108
+
109
+ metric.groups.each do |name, ms|
110
+ @payload[metric][name] ||= Airbrake::Stat.new
111
+ @payload[metric][name].increment_ms(ms)
112
+ end
113
+ end
114
+
115
+ def check_configuration(metric)
116
+ promise = @config.check_configuration
117
+ return promise if promise.rejected?
118
+
119
+ promise = @config.check_performance_options(metric)
120
+ return promise if promise.rejected?
82
121
 
83
- logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
122
+ if metric.timing && metric.timing == 0
123
+ return Promise.new.reject(':timing cannot be zero')
124
+ end
84
125
 
85
- with_grouped_payload(payload) do |resource_hash, destination|
126
+ Promise.new
127
+ end
128
+
129
+ def send(sender, payload, promise)
130
+ raise "payload cannot be empty. Race?" if payload.none?
131
+
132
+ with_grouped_payload(payload) do |metric_hash, destination|
86
133
  url = URI.join(
87
- @config.host,
88
- "api/v5/projects/#{@config.project_id}/#{destination}"
134
+ @config.apm_host,
135
+ "api/v5/projects/#{@config.project_id}/#{destination}",
89
136
  )
90
- @sender.send(resource_hash, promise, url)
137
+
138
+ logger.debug do
139
+ "#{LOG_LABEL} #{self.class.name}##{__method__}: #{metric_hash}"
140
+ end
141
+ sender.send(metric_hash, promise, url)
91
142
  end
92
143
 
93
144
  promise
94
145
  end
95
146
 
96
147
  def with_grouped_payload(raw_payload)
97
- grouped_payload = raw_payload.group_by do |resource, _stats|
98
- [resource.cargo, resource.destination]
148
+ grouped_payload = raw_payload.group_by do |metric, _stats|
149
+ [metric.cargo, metric.destination]
99
150
  end
100
151
 
101
- grouped_payload.each do |(cargo, destination), resources|
152
+ grouped_payload.each do |(cargo, destination), metrics|
102
153
  payload = {}
103
- payload[cargo] = serialize_resources(resources)
154
+ payload[cargo] = serialize_metrics(metrics)
104
155
  payload['environment'] = @config.environment if @config.environment
105
156
 
106
157
  yield(payload, destination)
107
158
  end
108
159
  end
109
160
 
110
- def serialize_resources(resources)
111
- resources.map do |resource, stats|
112
- resource_hash = resource.to_h.merge!(stats[:total].to_h)
161
+ def serialize_metrics(metrics)
162
+ metrics.map do |metric, stats|
163
+ metric_hash = metric.to_h.merge!(stats[:total].to_h)
113
164
 
114
- if resource.groups.any?
165
+ if metric.groups.any?
115
166
  group_stats = stats.reject { |name, _stat| name == :total }
116
- resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
167
+ metric_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
117
168
  stat.to_h
118
169
  end
119
170
  end
120
171
 
121
- resource_hash
172
+ metric_hash
122
173
  end
123
174
  end
124
175
  end
176
+ # rubocop:enable Metrics/ClassLength
125
177
  end
@@ -103,6 +103,7 @@ module Airbrake
103
103
  # needed for compatibility but it shouldn't exist in the future
104
104
  def value
105
105
  return @value['ok'] if resolved?
106
+
106
107
  @value
107
108
  end
108
109
  end
@@ -4,13 +4,15 @@ module Airbrake
4
4
  # @see Airbrake.notify_query
5
5
  # @api public
6
6
  # @since v3.2.0
7
- # rubocop:disable Metrics/ParameterLists, Metrics/BlockLength
8
- Query = Struct.new(
9
- :method, :route, :query, :func, :file, :line, :start_time, :end_time
10
- ) do
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Query
11
9
  include HashKeyable
12
10
  include Ignorable
13
11
  include Stashable
12
+ include Mergeable
13
+ include Grouppable
14
+
15
+ attr_accessor :method, :route, :query, :func, :file, :line, :timing, :time
14
16
 
15
17
  def initialize(
16
18
  method:,
@@ -19,10 +21,18 @@ module Airbrake
19
21
  func: nil,
20
22
  file: nil,
21
23
  line: nil,
22
- start_time:,
23
- end_time: Time.now
24
+ timing: nil,
25
+ time: Time.now
24
26
  )
25
- super(method, route, query, func, file, line, start_time, end_time)
27
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
28
+ @method = method
29
+ @route = route
30
+ @query = query
31
+ @func = func
32
+ @file = file
33
+ @line = line
34
+ @timing = timing
35
+ @time = time
26
36
  end
27
37
 
28
38
  def destination
@@ -33,21 +43,17 @@ module Airbrake
33
43
  'queries'
34
44
  end
35
45
 
36
- def groups
37
- {}
38
- end
39
-
40
46
  def to_h
41
47
  {
42
48
  'method' => method,
43
49
  'route' => route,
44
50
  'query' => query,
45
- 'time' => TimeTruncate.utc_truncate_minutes(start_time),
51
+ 'time' => @time_utc,
46
52
  'function' => func,
47
53
  'file' => file,
48
- 'line' => line
54
+ 'line' => line,
49
55
  }.delete_if { |_key, val| val.nil? }
50
56
  end
51
- # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
57
+ # rubocop:enable Metrics/ParameterLists
52
58
  end
53
59
  end
@@ -0,0 +1,65 @@
1
+ module Airbrake
2
+ # Queue represents a queue (worker).
3
+ #
4
+ # @see Airbrake.notify_queue
5
+ # @api public
6
+ # @since v4.9.0
7
+ class Queue
8
+ include HashKeyable
9
+ include Ignorable
10
+ include Stashable
11
+
12
+ attr_accessor :queue, :error_count, :groups, :timing, :time
13
+
14
+ def initialize(
15
+ queue:,
16
+ error_count:,
17
+ groups: {},
18
+ timing: nil,
19
+ time: Time.now
20
+ )
21
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
22
+ @queue = queue
23
+ @error_count = error_count
24
+ @groups = groups
25
+ @timing = timing
26
+ @time = time
27
+ end
28
+
29
+ def destination
30
+ 'queues-stats'
31
+ end
32
+
33
+ def cargo
34
+ 'queues'
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ 'queue' => queue,
40
+ 'errorCount' => error_count,
41
+ 'time' => @time_utc,
42
+ }
43
+ end
44
+
45
+ def hash
46
+ {
47
+ 'queue' => queue,
48
+ 'time' => @time_utc,
49
+ }.hash
50
+ end
51
+
52
+ def merge(other)
53
+ self.error_count += other.error_count
54
+ end
55
+
56
+ # Queues don't have routes, but we want to define this to make sure our
57
+ # filter API is consistent (other models define this property)
58
+ #
59
+ # @return [String] empty route
60
+ # @see https://github.com/airbrake/airbrake-ruby/pull/537
61
+ def route
62
+ ''
63
+ end
64
+ end
65
+ end