appsignal 3.4.4 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -1
  4. data/.semaphore/semaphore.yml +683 -52
  5. data/CHANGELOG.md +353 -4
  6. data/README.md +3 -0
  7. data/Rakefile +4 -2
  8. data/appsignal.gemspec +1 -1
  9. data/build_matrix.yml +27 -13
  10. data/ext/Rakefile +8 -1
  11. data/ext/agent.rb +27 -27
  12. data/ext/appsignal_extension.c +0 -24
  13. data/ext/base.rb +5 -2
  14. data/gemfiles/dry-monitor.gemfile +5 -0
  15. data/gemfiles/rails-7.1.gemfile +7 -0
  16. data/gemfiles/redis-4.gemfile +5 -0
  17. data/gemfiles/redis-5.gemfile +6 -0
  18. data/lib/appsignal/auth_check.rb +1 -1
  19. data/lib/appsignal/cli/diagnose/paths.rb +33 -10
  20. data/lib/appsignal/cli/diagnose.rb +15 -1
  21. data/lib/appsignal/config.rb +72 -7
  22. data/lib/appsignal/demo.rb +1 -1
  23. data/lib/appsignal/environment.rb +24 -13
  24. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +1 -1
  25. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +18 -0
  26. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +1 -1
  27. data/lib/appsignal/event_formatter.rb +2 -2
  28. data/lib/appsignal/extension/jruby.rb +4 -17
  29. data/lib/appsignal/extension.rb +1 -1
  30. data/lib/appsignal/heartbeat.rb +71 -0
  31. data/lib/appsignal/helpers/instrumentation.rb +10 -10
  32. data/lib/appsignal/helpers/metrics.rb +15 -13
  33. data/lib/appsignal/hooks/active_job.rb +9 -1
  34. data/lib/appsignal/hooks/active_support_notifications.rb +18 -9
  35. data/lib/appsignal/hooks/dry_monitor.rb +20 -0
  36. data/lib/appsignal/hooks/redis.rb +1 -0
  37. data/lib/appsignal/hooks/redis_client.rb +28 -0
  38. data/lib/appsignal/hooks.rb +4 -2
  39. data/lib/appsignal/integrations/active_support_notifications.rb +26 -0
  40. data/lib/appsignal/integrations/dry_monitor.rb +22 -0
  41. data/lib/appsignal/integrations/hanami.rb +1 -1
  42. data/lib/appsignal/integrations/padrino.rb +1 -1
  43. data/lib/appsignal/integrations/railtie.rb +28 -6
  44. data/lib/appsignal/integrations/redis_client.rb +20 -0
  45. data/lib/appsignal/integrations/sidekiq.rb +2 -2
  46. data/lib/appsignal/integrations/sinatra.rb +1 -1
  47. data/lib/appsignal/logger.rb +7 -5
  48. data/lib/appsignal/minutely.rb +4 -4
  49. data/lib/appsignal/probes/gvl.rb +1 -1
  50. data/lib/appsignal/probes/helpers.rb +1 -1
  51. data/lib/appsignal/probes/mri.rb +1 -1
  52. data/lib/appsignal/probes/sidekiq.rb +10 -8
  53. data/lib/appsignal/rack/generic_instrumentation.rb +1 -1
  54. data/lib/appsignal/rack/rails_instrumentation.rb +2 -2
  55. data/lib/appsignal/rack/sinatra_instrumentation.rb +5 -4
  56. data/lib/appsignal/rack/streaming_listener.rb +1 -1
  57. data/lib/appsignal/span.rb +2 -2
  58. data/lib/appsignal/transaction.rb +69 -14
  59. data/lib/appsignal/utils/deprecation_message.rb +2 -2
  60. data/lib/appsignal/utils/hash_sanitizer.rb +21 -9
  61. data/lib/appsignal/version.rb +1 -1
  62. data/lib/appsignal.rb +38 -31
  63. data/lib/puma/plugin/appsignal.rb +1 -1
  64. data/resources/cacert.pem +321 -159
  65. data/spec/lib/appsignal/capistrano2_spec.rb +2 -2
  66. data/spec/lib/appsignal/capistrano3_spec.rb +2 -2
  67. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +11 -0
  68. data/spec/lib/appsignal/cli/diagnose_spec.rb +70 -13
  69. data/spec/lib/appsignal/config_spec.rb +75 -18
  70. data/spec/lib/appsignal/environment_spec.rb +3 -3
  71. data/spec/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter_spec.rb +1 -1
  72. data/spec/lib/appsignal/event_formatter/rom/sql_formatter_spec.rb +22 -0
  73. data/spec/lib/appsignal/heartbeat_spec.rb +89 -0
  74. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +6 -0
  75. data/spec/lib/appsignal/hooks/activejob_spec.rb +26 -1
  76. data/spec/lib/appsignal/hooks/dry_monitor_spec.rb +104 -0
  77. data/spec/lib/appsignal/hooks/redis_client_spec.rb +238 -0
  78. data/spec/lib/appsignal/hooks/redis_spec.rb +98 -76
  79. data/spec/lib/appsignal/hooks/resque_spec.rb +1 -1
  80. data/spec/lib/appsignal/hooks_spec.rb +5 -5
  81. data/spec/lib/appsignal/integrations/railtie_spec.rb +128 -59
  82. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +20 -15
  83. data/spec/lib/appsignal/integrations/sinatra_spec.rb +2 -2
  84. data/spec/lib/appsignal/minutely_spec.rb +2 -2
  85. data/spec/lib/appsignal/probes/sidekiq_spec.rb +29 -6
  86. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +1 -1
  87. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +163 -71
  88. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +1 -0
  89. data/spec/lib/appsignal/transaction_spec.rb +139 -10
  90. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +42 -4
  91. data/spec/lib/appsignal_spec.rb +63 -61
  92. data/spec/lib/puma/appsignal_spec.rb +1 -1
  93. data/spec/spec_helper.rb +7 -7
  94. data/spec/support/fixtures/projects/valid/config/appsignal.yml +3 -3
  95. data/spec/support/helpers/config_helpers.rb +6 -2
  96. data/spec/support/helpers/dependency_helper.rb +13 -1
  97. data/spec/support/helpers/log_helpers.rb +2 -2
  98. data/spec/support/helpers/rails_helper.rb +28 -0
  99. data/spec/support/matchers/have_colorized_text.rb +1 -1
  100. metadata +19 -5
  101. data/ext/._appsignal-agent +0 -0
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Appsignal.logger.debug("Loading Rails (#{Rails.version}) integration")
3
+ Appsignal.internal_logger.debug("Loading Rails (#{Rails.version}) integration")
4
4
 
5
5
  require "appsignal/utils/rails_helper"
6
6
  require "appsignal/rack/rails_instrumentation"
@@ -36,9 +36,14 @@ module Appsignal
36
36
 
37
37
  Appsignal.start
38
38
 
39
- if Appsignal.config[:enable_rails_error_reporter] && Rails.respond_to?(:error) # rubocop:disable Style/GuardClause
40
- Rails.error.subscribe(Appsignal::Integrations::RailsErrorReporterSubscriber)
41
- end
39
+ initialize_error_reporter
40
+ end
41
+
42
+ def self.initialize_error_reporter
43
+ return unless Appsignal.config[:enable_rails_error_reporter]
44
+ return unless Rails.respond_to?(:error)
45
+
46
+ Rails.error.subscribe(Appsignal::Integrations::RailsErrorReporterSubscriber)
42
47
  end
43
48
  end
44
49
 
@@ -55,9 +60,14 @@ module Appsignal
55
60
  return unless handled
56
61
 
57
62
  Appsignal.send_error(error) do |transaction|
58
- namespace, action_name, tags = context_for(context.dup)
63
+ namespace, action_name, path, method, params, tags, custom_data =
64
+ context_for(context.dup)
59
65
  transaction.set_namespace(namespace) if namespace
60
66
  transaction.set_action(action_name) if action_name
67
+ transaction.set_metadata("path", path)
68
+ transaction.set_metadata("method", method)
69
+ transaction.params = params
70
+ transaction.set_sample_data("custom_data", custom_data) if custom_data
61
71
 
62
72
  tags[:severity] = severity
63
73
  tags[:source] = source.to_s if source
@@ -69,14 +79,23 @@ module Appsignal
69
79
 
70
80
  def context_for(context)
71
81
  tags = {}
82
+ custom_data = nil
72
83
 
73
84
  appsignal_context = context.delete(:appsignal)
74
85
  # Fetch the namespace and action name based on the Rails execution
75
86
  # context.
76
87
  controller = context.delete(:controller)
88
+ path = nil
89
+ method = nil
90
+ params = nil
77
91
  if controller
78
92
  namespace = Appsignal::Transaction::HTTP_REQUEST
79
93
  action_name = "#{controller.class.name}##{controller.action_name}"
94
+ unless controller.request.nil?
95
+ path = controller.request.path
96
+ method = controller.request.method
97
+ params = controller.request.filtered_parameters
98
+ end
80
99
  end
81
100
  # ActiveJob transaction naming relies on the current AppSignal
82
101
  # transaction namespace and action name copying done after this.
@@ -102,10 +121,13 @@ module Appsignal
102
121
 
103
122
  context_action_name = appsignal_context[:action]
104
123
  action_name = context_action_name if context_action_name
124
+
125
+ context_custom_data = appsignal_context[:custom_data]
126
+ custom_data = context_custom_data if context_custom_data
105
127
  end
106
128
  tags.merge!(context)
107
129
 
108
- [namespace, action_name, tags]
130
+ [namespace, action_name, path, method, params, tags, custom_data]
109
131
  end
110
132
  end
111
133
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ module RedisClientIntegration
6
+ def write(command)
7
+ sanitized_command =
8
+ if command[0] == :eval
9
+ "#{command[1]}#{" ?" * (command.size - 3)}"
10
+ else
11
+ "#{command[0]}#{" ?" * (command.size - 1)}"
12
+ end
13
+
14
+ Appsignal.instrument "query.redis", @config.id, sanitized_command do
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -9,7 +9,7 @@ module Appsignal
9
9
  #
10
10
  # @api private
11
11
  class SidekiqErrorHandler
12
- def call(exception, sidekiq_context)
12
+ def call(exception, sidekiq_context, _sidekiq_config = nil)
13
13
  transaction =
14
14
  if Appsignal::Transaction.current?
15
15
  Appsignal::Transaction.current
@@ -168,7 +168,7 @@ module Appsignal
168
168
  # Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
169
169
  # which haven't been loaded into memory yet so the YAML can't be
170
170
  # loaded.
171
- Appsignal.logger.warn "Unable to load YAML: #{error.message}"
171
+ Appsignal.internal_logger.warn "Unable to load YAML: #{error.message}"
172
172
  default
173
173
  end
174
174
  end
@@ -3,7 +3,7 @@
3
3
  require "appsignal"
4
4
  require "appsignal/rack/sinatra_instrumentation"
5
5
 
6
- Appsignal.logger.debug("Loading Sinatra (#{Sinatra::VERSION}) integration")
6
+ Appsignal.internal_logger.debug("Loading Sinatra (#{Sinatra::VERSION}) integration")
7
7
 
8
8
  app_settings = ::Sinatra::Application.settings
9
9
  Appsignal.config = Appsignal::Config.new(
@@ -17,6 +17,8 @@ module Appsignal
17
17
  FATAL => 7
18
18
  }.freeze
19
19
 
20
+ attr_reader :level
21
+
20
22
  # Create a new logger instance
21
23
  #
22
24
  # @param group Name of the group for this logger.
@@ -62,7 +64,7 @@ module Appsignal
62
64
  alias log add
63
65
 
64
66
  # Log a debug level message
65
- # @param message Mesage to log
67
+ # @param message Message to log
66
68
  # @param attributes Attributes to tag the log with
67
69
  # @return [void]
68
70
  def debug(message = nil, attributes = {})
@@ -75,7 +77,7 @@ module Appsignal
75
77
  end
76
78
 
77
79
  # Log an info level message
78
- # @param message Mesage to log
80
+ # @param message Message to log
79
81
  # @param attributes Attributes to tag the log with
80
82
  # @return [void]
81
83
  def info(message = nil, attributes = {})
@@ -88,7 +90,7 @@ module Appsignal
88
90
  end
89
91
 
90
92
  # Log a warn level message
91
- # @param message Mesage to log
93
+ # @param message Message to log
92
94
  # @param attributes Attributes to tag the log with
93
95
  # @return [void]
94
96
  def warn(message = nil, attributes = {})
@@ -101,7 +103,7 @@ module Appsignal
101
103
  end
102
104
 
103
105
  # Log an error level message
104
- # @param message Mesage to log
106
+ # @param message Message to log
105
107
  # @param attributes Attributes to tag the log with
106
108
  # @return [void]
107
109
  def error(message = nil, attributes = {})
@@ -114,7 +116,7 @@ module Appsignal
114
116
  end
115
117
 
116
118
  # Log a fatal level message
117
- # @param message Mesage to log
119
+ # @param message Message to log
118
120
  # @param attributes Attributes to tag the log with
119
121
  # @return [void]
120
122
  def fatal(message = nil, attributes = {})
@@ -108,7 +108,7 @@ module Appsignal
108
108
  attr_reader :probes
109
109
 
110
110
  def logger
111
- Appsignal.logger
111
+ Appsignal.internal_logger
112
112
  end
113
113
  end
114
114
 
@@ -132,7 +132,7 @@ module Appsignal
132
132
  sleep initial_wait_time
133
133
  initialize_probes
134
134
  loop do
135
- logger = Appsignal.logger
135
+ logger = Appsignal.internal_logger
136
136
  logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
137
137
  probe_instances.each do |name, probe|
138
138
  logger.debug("Gathering minutely metrics with '#{name}' probe")
@@ -181,13 +181,13 @@ module Appsignal
181
181
  klass = instance.class
182
182
  end
183
183
  unless dependencies_present?(klass)
184
- Appsignal.logger.debug "Skipping '#{name}' probe, " \
184
+ Appsignal.internal_logger.debug "Skipping '#{name}' probe, " \
185
185
  "#{klass}.dependency_present? returned falsy"
186
186
  return
187
187
  end
188
188
  probe_instances[name] = instance
189
189
  rescue => error
190
- logger = Appsignal.logger
190
+ logger = Appsignal.internal_logger
191
191
  logger.error "Error while initializing minutely probe '#{name}': #{error}"
192
192
  logger.debug error.backtrace.join("\n")
193
193
  end
@@ -22,7 +22,7 @@ module Appsignal
22
22
  end
23
23
 
24
24
  def initialize(appsignal: Appsignal, gvl_tools: ::GVLTools)
25
- Appsignal.logger.debug("Initializing GVL probe")
25
+ Appsignal.internal_logger.debug("Initializing GVL probe")
26
26
  @appsignal = appsignal
27
27
  @gvl_tools = gvl_tools
28
28
  end
@@ -47,7 +47,7 @@ module Appsignal
47
47
  # Auto detect hostname as fallback. May be inaccurate.
48
48
  @hostname =
49
49
  config[:hostname] || Socket.gethostname
50
- Appsignal.logger.debug "Probe helper: Using hostname config " \
50
+ Appsignal.internal_logger.debug "Probe helper: Using hostname config " \
51
51
  "option '#{@hostname.inspect}' as hostname"
52
52
 
53
53
  @hostname
@@ -11,7 +11,7 @@ module Appsignal
11
11
  end
12
12
 
13
13
  def initialize(appsignal: Appsignal, gc_profiler: Appsignal::GarbageCollection.profiler)
14
- Appsignal.logger.debug("Initializing VM probe")
14
+ Appsignal.internal_logger.debug("Initializing VM probe")
15
15
  @appsignal = appsignal
16
16
  @gc_profiler = gc_profiler
17
17
  end
@@ -59,7 +59,7 @@ module Appsignal
59
59
  @adapter = is_sidekiq7 ? Sidekiq7Adapter : Sidekiq6Adapter
60
60
 
61
61
  config_string = " with config: #{config}" unless config.empty?
62
- Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
62
+ Appsignal.internal_logger.debug("Initializing Sidekiq probe#{config_string}")
63
63
  require "sidekiq/api"
64
64
  end
65
65
 
@@ -78,9 +78,9 @@ module Appsignal
78
78
  redis_info = adapter.redis_info
79
79
  return unless redis_info
80
80
 
81
- gauge "connection_count", redis_info.fetch("connected_clients")
82
- gauge "memory_usage", redis_info.fetch("used_memory")
83
- gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
81
+ gauge "connection_count", redis_info["connected_clients"]
82
+ gauge "memory_usage", redis_info["used_memory"]
83
+ gauge "memory_usage_rss", redis_info["used_memory_rss"]
84
84
  end
85
85
 
86
86
  def track_stats
@@ -112,6 +112,8 @@ module Appsignal
112
112
 
113
113
  # Track a gauge metric with the `sidekiq_` prefix
114
114
  def gauge(key, value, tags = {})
115
+ return if value.nil?
116
+
115
117
  tags[:hostname] = hostname if hostname
116
118
  Appsignal.set_gauge "sidekiq_#{key}", value, tags
117
119
  end
@@ -121,14 +123,14 @@ module Appsignal
121
123
 
122
124
  if config.key?(:hostname)
123
125
  @hostname = config[:hostname]
124
- Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
125
- "option #{@hostname.inspect} as hostname"
126
+ Appsignal.internal_logger.debug "Sidekiq probe: Using hostname " \
127
+ "config option #{@hostname.inspect} as hostname"
126
128
  return @hostname
127
129
  end
128
130
 
129
131
  host = adapter.hostname
130
- Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
131
- "#{host.inspect} as hostname"
132
+ Appsignal.internal_logger.debug "Sidekiq probe: Using Redis server " \
133
+ "hostname #{host.inspect} as hostname"
132
134
  @hostname = host
133
135
  end
134
136
  end
@@ -7,7 +7,7 @@ module Appsignal
7
7
  module Rack
8
8
  class GenericInstrumentation
9
9
  def initialize(app, options = {})
10
- Appsignal.logger.debug "Initializing Appsignal::Rack::GenericInstrumentation"
10
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::GenericInstrumentation"
11
11
  @app = app
12
12
  @options = options
13
13
  end
@@ -7,7 +7,7 @@ module Appsignal
7
7
  module Rack
8
8
  class RailsInstrumentation
9
9
  def initialize(app, options = {})
10
- Appsignal.logger.debug "Initializing Appsignal::Rack::RailsInstrumentation"
10
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::RailsInstrumentation"
11
11
  @app = app
12
12
  @options = options
13
13
  end
@@ -43,7 +43,7 @@ module Appsignal
43
43
  begin
44
44
  transaction.set_metadata("method", request.request_method)
45
45
  rescue => error
46
- Appsignal.logger.error("Unable to report HTTP request method: '#{error}'")
46
+ Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
47
47
  end
48
48
  Appsignal::Transaction.complete_current!
49
49
  end
@@ -15,7 +15,7 @@ module Appsignal
15
15
  def initialize(app, options = {})
16
16
  @app = app
17
17
  @options = options
18
- Appsignal.logger.warn "Please remove Appsignal::Rack::SinatraInstrumentation " \
18
+ Appsignal.internal_logger.warn "Please remove Appsignal::Rack::SinatraInstrumentation " \
19
19
  "from your Sinatra::Base class. This is no longer needed."
20
20
  end
21
21
 
@@ -32,7 +32,7 @@ module Appsignal
32
32
  attr_reader :raise_errors_on
33
33
 
34
34
  def initialize(app, options = {})
35
- Appsignal.logger.debug "Initializing Appsignal::Rack::SinatraBaseInstrumentation"
35
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::SinatraBaseInstrumentation"
36
36
  @app = app
37
37
  @options = options
38
38
  @raise_errors_on = raise_errors?(@app)
@@ -47,13 +47,14 @@ module Appsignal
47
47
  end
48
48
 
49
49
  def call_with_appsignal_monitoring(env)
50
- env[:params_method] = @options[:params_method] if @options[:params_method]
50
+ options = { :force => @options.include?(:force) && @options[:force] }
51
+ options.merge!(:params_method => @options[:params_method]) if @options[:params_method]
51
52
  request = @options.fetch(:request_class, Sinatra::Request).new(env)
52
53
  transaction = Appsignal::Transaction.create(
53
54
  SecureRandom.uuid,
54
55
  Appsignal::Transaction::HTTP_REQUEST,
55
56
  request,
56
- :force => @options.include?(:force) && @options[:force]
57
+ options
57
58
  )
58
59
  begin
59
60
  Appsignal.instrument("process_action.sinatra") do
@@ -7,7 +7,7 @@ module Appsignal
7
7
  # @api private
8
8
  class StreamingListener
9
9
  def initialize(app, options = {})
10
- Appsignal.logger.debug "Initializing Appsignal::Rack::StreamingListener"
10
+ Appsignal.internal_logger.debug "Initializing Appsignal::Rack::StreamingListener"
11
11
  @app = app
12
12
  @options = options
13
13
  end
@@ -16,8 +16,8 @@ module Appsignal
16
16
 
17
17
  def add_error(error)
18
18
  unless error.is_a?(Exception)
19
- Appsignal.logger.error "Appsignal::Span#add_error: Cannot add error. " \
20
- "The given value is not an exception: #{error.inspect}"
19
+ Appsignal.internal_logger.error "Appsignal::Span#add_error: Cannot " \
20
+ "add error. The given value is not an exception: #{error.inspect}"
21
21
  return
22
22
  end
23
23
  return unless error
@@ -12,6 +12,7 @@ module Appsignal
12
12
  ALLOWED_TAG_KEY_TYPES = [Symbol, String].freeze
13
13
  ALLOWED_TAG_VALUE_TYPES = [Symbol, String, Integer].freeze
14
14
  BREADCRUMB_LIMIT = 20
15
+ ERROR_CAUSES_LIMIT = 10
15
16
 
16
17
  class << self
17
18
  def create(id, namespace, request, options = {})
@@ -25,7 +26,7 @@ module Appsignal
25
26
  Appsignal::Transaction.new(id, namespace, request, options)
26
27
  else
27
28
  # Otherwise, log the issue about trying to start another transaction
28
- Appsignal.logger.warn_once_then_debug(
29
+ Appsignal.internal_logger.warn_once_then_debug(
29
30
  :transaction_id,
30
31
  "Trying to start new transaction with id " \
31
32
  "'#{id}', but a transaction with id '#{current.transaction_id}' " \
@@ -58,7 +59,7 @@ module Appsignal
58
59
  def complete_current!
59
60
  current.complete
60
61
  rescue => e
61
- Appsignal.logger.error(
62
+ Appsignal.internal_logger.error(
62
63
  "Failed to complete transaction ##{current.transaction_id}. #{e.message}"
63
64
  )
64
65
  ensure
@@ -113,7 +114,7 @@ module Appsignal
113
114
 
114
115
  def complete
115
116
  if discarded?
116
- Appsignal.logger.debug "Skipping transaction '#{transaction_id}' " \
117
+ Appsignal.internal_logger.debug "Skipping transaction '#{transaction_id}' " \
117
118
  "because it was manually discarded."
118
119
  return
119
120
  end
@@ -186,6 +187,12 @@ module Appsignal
186
187
  # @see https://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html
187
188
  # Breadcrumb reference
188
189
  def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.now.utc)
190
+ unless metadata.is_a? Hash
191
+ Appsignal.internal_logger.error "add_breadcrumb: Cannot add breadcrumb. " \
192
+ "The given metadata argument is not a Hash."
193
+ return
194
+ end
195
+
189
196
  @breadcrumbs.push(
190
197
  :time => time.to_i,
191
198
  :category => category,
@@ -280,7 +287,7 @@ module Appsignal
280
287
 
281
288
  @ext.set_queue_start(start)
282
289
  rescue RangeError
283
- Appsignal.logger.warn("Queue start value #{start} is too big")
290
+ Appsignal.internal_logger.warn("Queue start value #{start} is too big")
284
291
  end
285
292
 
286
293
  # Set the queue time based on the HTTP header or `:queue_start` env key
@@ -308,12 +315,20 @@ module Appsignal
308
315
 
309
316
  def set_metadata(key, value)
310
317
  return unless key && value
318
+ return if Appsignal.config[:filter_metadata].include?(key.to_s)
311
319
 
312
320
  @ext.set_metadata(key, value)
313
321
  end
314
322
 
315
323
  def set_sample_data(key, data)
316
- return unless key && data && (data.is_a?(Array) || data.is_a?(Hash))
324
+ return unless key && data
325
+
326
+ if !data.is_a?(Array) && !data.is_a?(Hash)
327
+ Appsignal.internal_logger.error(
328
+ "Invalid sample data for '#{key}'. Value is not an Array or Hash: '#{data.inspect}'"
329
+ )
330
+ return
331
+ end
317
332
 
318
333
  @ext.set_sample_data(
319
334
  key.to_s,
@@ -322,11 +337,11 @@ module Appsignal
322
337
  rescue RuntimeError => e
323
338
  begin
324
339
  inspected_data = data.inspect
325
- Appsignal.logger.error(
340
+ Appsignal.internal_logger.error(
326
341
  "Error generating data (#{e.class}: #{e.message}) for '#{inspected_data}'"
327
342
  )
328
343
  rescue => e
329
- Appsignal.logger.error(
344
+ Appsignal.internal_logger.error(
330
345
  "Error generating data (#{e.class}: #{e.message}). Can't inspect data."
331
346
  )
332
347
  end
@@ -337,7 +352,7 @@ module Appsignal
337
352
  :params => sanitized_params,
338
353
  :environment => sanitized_environment,
339
354
  :session_data => sanitized_session_data,
340
- :metadata => metadata,
355
+ :metadata => sanitized_metadata,
341
356
  :tags => sanitized_tags,
342
357
  :breadcrumbs => breadcrumbs
343
358
  }.each do |key, data|
@@ -347,7 +362,7 @@ module Appsignal
347
362
 
348
363
  def set_error(error)
349
364
  unless error.is_a?(Exception)
350
- Appsignal.logger.error "Appsignal::Transaction#set_error: Cannot set error. " \
365
+ Appsignal.internal_logger.error "Appsignal::Transaction#set_error: Cannot set error. " \
351
366
  "The given value is not an exception: #{error.inspect}"
352
367
  return
353
368
  end
@@ -360,6 +375,41 @@ module Appsignal
360
375
  cleaned_error_message(error),
361
376
  backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new
362
377
  )
378
+
379
+ root_cause_missing = false
380
+
381
+ causes = []
382
+ while error
383
+ error = error.cause
384
+
385
+ break unless error
386
+
387
+ if causes.length >= ERROR_CAUSES_LIMIT
388
+ Appsignal.internal_logger.debug "Appsignal::Transaction#set_error: Error has more " \
389
+ "than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
390
+ "will be reported."
391
+ root_cause_missing = true
392
+ break
393
+ end
394
+
395
+ causes << error
396
+ end
397
+
398
+ return if causes.empty?
399
+
400
+ causes_sample_data = causes.map do |e|
401
+ {
402
+ :name => e.class.name,
403
+ :message => cleaned_error_message(e)
404
+ }
405
+ end
406
+
407
+ causes_sample_data.last[:is_root_cause] = false if root_cause_missing
408
+
409
+ set_sample_data(
410
+ "error_causes",
411
+ causes_sample_data
412
+ )
363
413
  end
364
414
  alias_method :add_exception, :set_error
365
415
 
@@ -479,7 +529,7 @@ module Appsignal
479
529
  request.send options[:params_method]
480
530
  rescue => e
481
531
  # Getting params from the request has been know to fail.
482
- Appsignal.logger.debug "Exception while getting params: #{e}"
532
+ Appsignal.internal_logger.debug "Exception while getting params: #{e}"
483
533
  nil
484
534
  end
485
535
  end
@@ -522,12 +572,17 @@ module Appsignal
522
572
  )
523
573
  end
524
574
 
525
- # Returns metadata from the environment.
575
+ # Returns sanitized metadata set by {#set_metadata} and from the
576
+ # {#environment}.
526
577
  #
527
- # @return [nil] if no `:metadata` key is present in the {#environment}.
528
578
  # @return [Hash<String, Object>]
529
- def metadata
530
- environment[:metadata]
579
+ def sanitized_metadata
580
+ metadata = environment[:metadata]
581
+ return unless metadata
582
+
583
+ metadata
584
+ .transform_keys(&:to_s)
585
+ .reject { |key, _value| Appsignal.config[:filter_metadata].include?(key) }
531
586
  end
532
587
 
533
588
  # Returns the environment for a transaction.
@@ -3,12 +3,12 @@
3
3
  module Appsignal
4
4
  module Utils
5
5
  module DeprecationMessage
6
- def self.message(message, logger = Appsignal.logger)
6
+ def self.message(message, logger = Appsignal.internal_logger)
7
7
  Kernel.warn "appsignal WARNING: #{message}"
8
8
  logger.warn message
9
9
  end
10
10
 
11
- def deprecation_message(message, logger = Appsignal.logger)
11
+ def deprecation_message(message, logger = Appsignal.internal_logger)
12
12
  Appsignal::Utils::DeprecationMessage.message(message, logger)
13
13
  end
14
14
  end
@@ -5,20 +5,21 @@ module Appsignal
5
5
  # @api private
6
6
  class HashSanitizer
7
7
  FILTERED = "[FILTERED]"
8
+ RECURSIVE = "[RECURSIVE VALUE]"
8
9
 
9
10
  class << self
10
11
  def sanitize(value, filter_keys = [])
11
- sanitize_value(value, filter_keys)
12
+ sanitize_value(value, filter_keys, [])
12
13
  end
13
14
 
14
15
  private
15
16
 
16
- def sanitize_value(value, filter_keys)
17
+ def sanitize_value(value, filter_keys, seen)
17
18
  case value
18
19
  when Hash
19
- sanitize_hash(value, filter_keys)
20
+ sanitize_hash(value, filter_keys, seen)
20
21
  when Array
21
- sanitize_array(value, filter_keys)
22
+ sanitize_array(value, filter_keys, seen)
22
23
  when TrueClass, FalseClass, NilClass, Integer, String, Symbol, Float
23
24
  unmodified(value)
24
25
  else
@@ -26,23 +27,34 @@ module Appsignal
26
27
  end
27
28
  end
28
29
 
29
- def sanitize_hash(source, filter_keys)
30
+ def sanitize_hash(source, filter_keys, seen)
31
+ seen = seen.clone << source.object_id
32
+
30
33
  {}.tap do |hash|
31
34
  source.each_pair do |key, value|
32
35
  hash[key] =
33
- if filter_keys.include?(key.to_s)
36
+ if seen.include?(value.object_id)
37
+ RECURSIVE
38
+ elsif filter_keys.include?(key.to_s)
34
39
  FILTERED
35
40
  else
36
- sanitize_value(value, filter_keys)
41
+ sanitize_value(value, filter_keys, seen)
37
42
  end
38
43
  end
39
44
  end
40
45
  end
41
46
 
42
- def sanitize_array(source, filter_keys)
47
+ def sanitize_array(source, filter_keys, seen)
48
+ seen = seen.clone << source.object_id
49
+
43
50
  [].tap do |array|
44
51
  source.each_with_index do |item, index|
45
- array[index] = sanitize_value(item, filter_keys)
52
+ array[index] =
53
+ if seen.include?(item.object_id)
54
+ RECURSIVE
55
+ else
56
+ sanitize_value(item, filter_keys, seen)
57
+ end
46
58
  end
47
59
  end
48
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.4.4"
4
+ VERSION = "3.7.0"
5
5
  end