appsignal 2.11.0.beta.5 → 2.11.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +197 -0
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +16 -1
  5. data/Rakefile +11 -5
  6. data/build_matrix.yml +13 -0
  7. data/ext/agent.yml +17 -25
  8. data/ext/appsignal_extension.c +1 -1
  9. data/ext/base.rb +12 -9
  10. data/gemfiles/no_dependencies.gemfile +7 -0
  11. data/gemfiles/resque-2.gemfile +0 -1
  12. data/gemfiles/webmachine.gemfile +1 -0
  13. data/lib/appsignal/cli/diagnose/utils.rb +8 -11
  14. data/lib/appsignal/cli/install.rb +5 -8
  15. data/lib/appsignal/config.rb +47 -15
  16. data/lib/appsignal/helpers/instrumentation.rb +32 -0
  17. data/lib/appsignal/hooks.rb +1 -0
  18. data/lib/appsignal/hooks/action_mailer.rb +22 -0
  19. data/lib/appsignal/hooks/active_support_notifications.rb +72 -0
  20. data/lib/appsignal/hooks/shoryuken.rb +43 -4
  21. data/lib/appsignal/transaction.rb +30 -2
  22. data/lib/appsignal/version.rb +1 -1
  23. data/spec/lib/appsignal/config_spec.rb +18 -1
  24. data/spec/lib/appsignal/hooks/action_mailer_spec.rb +54 -0
  25. data/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb +35 -0
  26. data/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb +145 -0
  27. data/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb +69 -0
  28. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +9 -137
  29. data/spec/lib/appsignal/hooks/resque_spec.rb +10 -2
  30. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +151 -104
  31. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +4 -2
  32. data/spec/lib/appsignal/transaction_spec.rb +55 -0
  33. data/spec/lib/appsignal_spec.rb +30 -0
  34. data/spec/support/helpers/dependency_helper.rb +4 -0
  35. metadata +14 -5
@@ -485,7 +485,7 @@ static VALUE data_append_boolean(VALUE self, VALUE value) {
485
485
  return Qnil;
486
486
  }
487
487
 
488
- static VALUE data_append_nil(VALUE self, VALUE value) {
488
+ static VALUE data_append_nil(VALUE self) {
489
489
  appsignal_data_t* data;
490
490
 
491
491
  Data_Get_Struct(self, appsignal_data_t, data);
@@ -119,30 +119,33 @@ def download_archive(type)
119
119
 
120
120
  version = AGENT_CONFIG["version"]
121
121
  filename = ARCH_CONFIG[type]["filename"]
122
- attempted_mirror_urls = []
122
+ download_errors = []
123
123
 
124
124
  AGENT_CONFIG["mirrors"].each do |mirror|
125
125
  download_url = [mirror, version, filename].join("/")
126
- attempted_mirror_urls << download_url
127
126
  report["download"]["download_url"] = download_url
128
127
 
129
128
  begin
130
- return open(
129
+ args = [
131
130
  download_url,
132
131
  :ssl_ca_cert => CA_CERT_PATH,
133
132
  :proxy => http_proxy
134
- )
135
- rescue
133
+ ]
134
+ if URI.respond_to?(:open) # rubocop:disable Style/GuardClause
135
+ return URI.open(*args)
136
+ else
137
+ return open(*args)
138
+ end
139
+ rescue => error
140
+ download_errors << "- URL: #{download_url}\n Error: #{error.class}: #{error.message}"
136
141
  next
137
142
  end
138
143
  end
139
144
 
140
- attempted_mirror_urls_mapped = attempted_mirror_urls.map { |mirror| "- #{mirror}" }
141
145
  abort_installation(
142
146
  "Could not download archive from any of our mirrors. " \
143
- "Attempted to download the archive from the following urls:\n" \
144
- "#{attempted_mirror_urls_mapped.join("\n")}\n" \
145
- "Please make sure your network allows access to any of these mirrors."
147
+ "Please make sure your network allows access to any of these mirrors.\n" \
148
+ "Attempted to download the archive from the following urls:\n#{download_errors.join("\n")}"
146
149
  )
147
150
  end
148
151
 
@@ -2,4 +2,11 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'rack', '~> 1.6'
4
4
 
5
+ ruby_version = Gem::Version.new(RUBY_VERSION)
6
+ if ruby_version < Gem::Version.new("2.0.0")
7
+ # Newer versions of this gem have rexml as a dependency which doesn't work on
8
+ # Ruby 1.9
9
+ gem "crack", "0.4.4"
10
+ end
11
+
5
12
  gemspec :path => '../'
@@ -2,7 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'resque', "~> 2.0"
4
4
  gem 'sinatra'
5
- gem 'mime-types', '~> 2.6'
6
5
 
7
6
  gemspec :path => '../'
8
7
 
@@ -1,5 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'webmachine'
4
+ gem 'webrick'
4
5
 
5
6
  gemspec :path => '../'
@@ -34,20 +34,17 @@ module Appsignal
34
34
  end
35
35
 
36
36
  def self.parse_yaml(contents)
37
- arguments = [contents]
38
37
  if YAML.respond_to? :safe_load
39
- method = :safe_load
40
- arguments << \
41
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
42
- # Use keyword params for Ruby 2.6 and up
43
- { :permitted_classes => [Time] }
44
- else
45
- [Time]
46
- end
38
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
39
+ # Use keyword params for Ruby 2.6 and up
40
+ YAML.safe_load(contents, :permitted_classes => [Time])
41
+ else
42
+ YAML.safe_load(contents, [Time])
43
+ end
47
44
  else
48
- method = :load
45
+ # Support for Ruby versions without YAML.safe_load
46
+ YAML.load(contents) # rubocop:disable Security/YAMLLoad
49
47
  end
50
- YAML.send(method, *arguments)
51
48
  end
52
49
  end
53
50
  end
@@ -278,14 +278,11 @@ module Appsignal
278
278
  "../../../resources/appsignal.yml.erb"
279
279
  )
280
280
  file_contents = File.read(filename)
281
- arguments = [file_contents]
282
- if ruby_2_6_or_up?
283
- arguments << { :trim_mode => "-" }
284
- else
285
- arguments << nil
286
- arguments << "-"
287
- end
288
- template = ERB.new(*arguments)
281
+ template = if ruby_2_6_or_up?
282
+ ERB.new(file_contents, :trim_mode => "-")
283
+ else
284
+ ERB.new(file_contents, nil, "-")
285
+ end
289
286
  config = template.result(OpenStruct.new(data).instance_eval { binding })
290
287
 
291
288
  FileUtils.mkdir_p(File.join(Dir.pwd, "config"))
@@ -81,6 +81,50 @@ module Appsignal
81
81
  "APPSIGNAL_TRANSACTION_DEBUG_MODE" => :transaction_debug_mode,
82
82
  "APP_REVISION" => :revision
83
83
  }.freeze
84
+ # @api private
85
+ ENV_STRING_KEYS = %w[
86
+ APPSIGNAL_APP_NAME
87
+ APPSIGNAL_CA_FILE_PATH
88
+ APPSIGNAL_DNS_SERVERS
89
+ APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH
90
+ APPSIGNAL_HOSTNAME
91
+ APPSIGNAL_HTTP_PROXY
92
+ APPSIGNAL_LOG
93
+ APPSIGNAL_LOG_PATH
94
+ APPSIGNAL_PUSH_API_ENDPOINT
95
+ APPSIGNAL_PUSH_API_KEY
96
+ APPSIGNAL_WORKING_DIRECTORY_PATH
97
+ APPSIGNAL_WORKING_DIR_PATH
98
+ APP_REVISION
99
+ ].freeze
100
+ # @api private
101
+ ENV_BOOLEAN_KEYS = %w[
102
+ APPSIGNAL_ACTIVE
103
+ APPSIGNAL_DEBUG
104
+ APPSIGNAL_ENABLE_ALLOCATION_TRACKING
105
+ APPSIGNAL_ENABLE_FRONTEND_ERROR_CATCHING
106
+ APPSIGNAL_ENABLE_GC_INSTRUMENTATION
107
+ APPSIGNAL_ENABLE_HOST_METRICS
108
+ APPSIGNAL_ENABLE_MINUTELY_PROBES
109
+ APPSIGNAL_FILES_WORLD_ACCESSIBLE
110
+ APPSIGNAL_INSTRUMENT_NET_HTTP
111
+ APPSIGNAL_INSTRUMENT_REDIS
112
+ APPSIGNAL_INSTRUMENT_SEQUEL
113
+ APPSIGNAL_RUNNING_IN_CONTAINER
114
+ APPSIGNAL_SEND_ENVIRONMENT_METADATA
115
+ APPSIGNAL_SEND_PARAMS
116
+ APPSIGNAL_SKIP_SESSION_DATA
117
+ APPSIGNAL_TRANSACTION_DEBUG_MODE
118
+ ].freeze
119
+ # @api private
120
+ ENV_ARRAY_KEYS = %w[
121
+ APPSIGNAL_FILTER_PARAMETERS
122
+ APPSIGNAL_FILTER_SESSION_DATA
123
+ APPSIGNAL_IGNORE_ACTIONS
124
+ APPSIGNAL_IGNORE_ERRORS
125
+ APPSIGNAL_IGNORE_NAMESPACES
126
+ APPSIGNAL_REQUEST_HEADERS
127
+ ].freeze
84
128
 
85
129
  # Mapping of old and deprecated AppSignal configuration keys
86
130
  DEPRECATED_CONFIG_KEY_MAPPING = {
@@ -358,33 +402,21 @@ module Appsignal
358
402
  config = {}
359
403
 
360
404
  # Configuration with string type
361
- %w[APPSIGNAL_PUSH_API_KEY APPSIGNAL_APP_NAME APPSIGNAL_PUSH_API_ENDPOINT
362
- APPSIGNAL_FRONTEND_ERROR_CATCHING_PATH APPSIGNAL_HTTP_PROXY
363
- APPSIGNAL_LOG APPSIGNAL_LOG_PATH APPSIGNAL_WORKING_DIR_PATH
364
- APPSIGNAL_HOSTNAME APPSIGNAL_CA_FILE_PATH APP_REVISION].each do |var|
405
+ ENV_STRING_KEYS.each do |var|
365
406
  env_var = ENV[var]
366
407
  next unless env_var
367
408
  config[ENV_TO_KEY_MAPPING[var]] = env_var
368
409
  end
369
410
 
370
411
  # Configuration with boolean type
371
- %w[APPSIGNAL_ACTIVE APPSIGNAL_DEBUG APPSIGNAL_INSTRUMENT_NET_HTTP
372
- APPSIGNAL_INSTRUMENT_REDIS APPSIGNAL_INSTRUMENT_SEQUEL
373
- APPSIGNAL_SKIP_SESSION_DATA APPSIGNAL_ENABLE_FRONTEND_ERROR_CATCHING
374
- APPSIGNAL_ENABLE_ALLOCATION_TRACKING APPSIGNAL_ENABLE_GC_INSTRUMENTATION
375
- APPSIGNAL_RUNNING_IN_CONTAINER APPSIGNAL_ENABLE_HOST_METRICS
376
- APPSIGNAL_SEND_ENVIRONMENT_METADATA APPSIGNAL_SEND_PARAMS
377
- APPSIGNAL_ENABLE_MINUTELY_PROBES APPSIGNAL_FILES_WORLD_ACCESSIBLE
378
- APPSIGNAL_TRANSACTION_DEBUG_MODE].each do |var|
412
+ ENV_BOOLEAN_KEYS.each do |var|
379
413
  env_var = ENV[var]
380
414
  next unless env_var
381
415
  config[ENV_TO_KEY_MAPPING[var]] = env_var.casecmp("true").zero?
382
416
  end
383
417
 
384
418
  # Configuration with array of strings type
385
- %w[APPSIGNAL_IGNORE_ACTIONS APPSIGNAL_IGNORE_ERRORS
386
- APPSIGNAL_IGNORE_NAMESPACES APPSIGNAL_FILTER_PARAMETERS
387
- APPSIGNAL_FILTER_SESSION_DATA APPSIGNAL_REQUEST_HEADERS].each do |var|
419
+ ENV_ARRAY_KEYS.each do |var|
388
420
  env_var = ENV[var]
389
421
  next unless env_var
390
422
  config[ENV_TO_KEY_MAPPING[var]] = env_var.split(",")
@@ -380,6 +380,38 @@ module Appsignal
380
380
  end
381
381
  alias :tag_job :tag_request
382
382
 
383
+ # Add breadcrumbs to the transaction.
384
+ #
385
+ # Breadcrumbs can be used to trace what path a user has taken
386
+ # before encounterin an error.
387
+ #
388
+ # Only the last 20 added breadcrumbs will be saved.
389
+ #
390
+ # @example
391
+ # Appsignal.add_breadcrumb("Navigation", "http://blablabla.com", "", { :response => 200 }, Time.now.utc)
392
+ # Appsignal.add_breadcrumb("Network", "[GET] http://blablabla.com", "", { :response => 500 })
393
+ # Appsignal.add_breadcrumb("UI", "closed modal(change_password)", "User closed modal without actions")
394
+ #
395
+ # @param category [String] category of breadcrumb
396
+ # e.g. "UI", "Network", "Navigation", "Console".
397
+ # @param action [String] name of breadcrumb
398
+ # e.g "The user clicked a button", "HTTP 500 from http://blablabla.com"
399
+ # @option message [String] optional message in string format
400
+ # @option metadata [Hash<String,String>] key/value metadata in <string, string> format
401
+ # @option time [Time] time of breadcrumb, should respond to `.to_i` defaults to `Time.now.utc`
402
+ # @return [void]
403
+ #
404
+ # @see Transaction#add_breadcrumb
405
+ # @see http://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html
406
+ # Breadcrumb reference
407
+ # @since 2.12.0
408
+ def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.now.utc)
409
+ return unless active?
410
+ transaction = Appsignal::Transaction.current
411
+ return false unless transaction
412
+ transaction.add_breadcrumb(category, action, message, metadata, time)
413
+ end
414
+
383
415
  # Instrument helper for AppSignal.
384
416
  #
385
417
  # For more help, read our custom instrumentation guide, listed under "See
@@ -96,6 +96,7 @@ module Appsignal
96
96
  end
97
97
 
98
98
  require "appsignal/hooks/action_cable"
99
+ require "appsignal/hooks/action_mailer"
99
100
  require "appsignal/hooks/active_job"
100
101
  require "appsignal/hooks/active_support_notifications"
101
102
  require "appsignal/hooks/celluloid"
@@ -0,0 +1,22 @@
1
+ module Appsignal
2
+ class Hooks
3
+ class ActionMailerHook < Appsignal::Hooks::Hook
4
+ register :action_mailer
5
+
6
+ def dependencies_present?
7
+ defined?(::ActionMailer)
8
+ end
9
+
10
+ def install
11
+ ActiveSupport::Notifications
12
+ .subscribe("process.action_mailer") do |_, _, _, _, payload|
13
+ Appsignal.increment_counter(
14
+ :action_mailer_process,
15
+ 1,
16
+ :mailer => payload[:mailer], :action => payload[:action]
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -23,6 +23,21 @@ module Appsignal
23
23
  end
24
24
  end
25
25
 
26
+ instrumenter = ::ActiveSupport::Notifications::Instrumenter
27
+
28
+ if instrumenter.method_defined?(:start) && instrumenter.method_defined?(:finish)
29
+ install_start_finish
30
+ else
31
+ install_instrument
32
+ end
33
+
34
+ # rubocop:disable Style/GuardClause
35
+ if instrumenter.method_defined?(:finish_with_state)
36
+ install_finish_with_state
37
+ end
38
+ end
39
+
40
+ def install_instrument
26
41
  ::ActiveSupport::Notifications::Instrumenter.class_eval do
27
42
  alias instrument_without_appsignal instrument
28
43
 
@@ -46,6 +61,63 @@ module Appsignal
46
61
  end
47
62
  end
48
63
  end
64
+
65
+ def install_start_finish
66
+ ::ActiveSupport::Notifications::Instrumenter.class_eval do
67
+ alias start_without_appsignal start
68
+
69
+ def start(name, payload = {})
70
+ # Events that start with a bang are internal to Rails
71
+ instrument_this = name[0] != BANG
72
+
73
+ Appsignal::Transaction.current.start_event if instrument_this
74
+
75
+ start_without_appsignal(name, payload)
76
+ end
77
+
78
+ alias finish_without_appsignal finish
79
+
80
+ def finish(name, payload = {})
81
+ # Events that start with a bang are internal to Rails
82
+ instrument_this = name[0] != BANG
83
+
84
+ if instrument_this
85
+ title, body, body_format = Appsignal::EventFormatter.format(name, payload)
86
+ Appsignal::Transaction.current.finish_event(
87
+ name.to_s,
88
+ title,
89
+ body,
90
+ body_format
91
+ )
92
+ end
93
+
94
+ finish_without_appsignal(name, payload)
95
+ end
96
+ end
97
+ end
98
+
99
+ def install_finish_with_state
100
+ ::ActiveSupport::Notifications::Instrumenter.class_eval do
101
+ alias finish_with_state_without_appsignal finish_with_state
102
+
103
+ def finish_with_state(listeners_state, name, payload = {})
104
+ # Events that start with a bang are internal to Rails
105
+ instrument_this = name[0] != BANG
106
+
107
+ if instrument_this
108
+ title, body, body_format = Appsignal::EventFormatter.format(name, payload)
109
+ Appsignal::Transaction.current.finish_event(
110
+ name.to_s,
111
+ title,
112
+ body,
113
+ body_format
114
+ )
115
+ end
116
+
117
+ finish_with_state_without_appsignal(listeners_state, name, payload)
118
+ end
119
+ end
120
+ end
49
121
  end
50
122
  end
51
123
  end
@@ -5,21 +5,60 @@ module Appsignal
5
5
  # @api private
6
6
  class ShoryukenMiddleware
7
7
  def call(worker_instance, queue, sqs_msg, body)
8
- metadata = { :queue => queue }.merge(sqs_msg.attributes)
8
+ batch = sqs_msg.is_a?(Array)
9
+ attributes =
10
+ if batch
11
+ # We can't instrument batched message separately, the `yield` will
12
+ # perform all the batched messages.
13
+ # To provide somewhat useful metadata, Get first message based on
14
+ # SentTimestamp, and use its attributes as metadata for the
15
+ # transaction. We can't combine them all because then they would
16
+ # overwrite each other and the last message (in an sorted order)
17
+ # would be used as the source of the metadata. With the
18
+ # oldest/first message at least some useful information is stored
19
+ # such as the first received time and the number of retries for the
20
+ # first message. The newer message should have lower values and
21
+ # timestamps in their metadata.
22
+ first_msg = sqs_msg.min do |a, b|
23
+ a.attributes["SentTimestamp"].to_i <=> b.attributes["SentTimestamp"].to_i
24
+ end
25
+ # Add batch => true metadata so people can recognize when a
26
+ # transaction is about a batch of messages.
27
+ first_msg.attributes.merge(:batch => true)
28
+ else
29
+ sqs_msg.attributes.merge(:message_id => sqs_msg.message_id)
30
+ end
31
+ metadata = { :queue => queue }.merge(attributes)
9
32
  options = {
10
33
  :class => worker_instance.class.name,
11
34
  :method => "perform",
12
35
  :metadata => metadata
13
36
  }
14
37
 
15
- args = body.is_a?(Hash) ? body : { :params => body }
38
+ args =
39
+ if batch
40
+ bodies = {}
41
+ sqs_msg.each_with_index do |msg, index|
42
+ # Store all separate bodies on a hash with the key being the
43
+ # message_id
44
+ bodies[msg.message_id] = body[index]
45
+ end
46
+ bodies
47
+ else
48
+ case body
49
+ when Hash
50
+ body
51
+ else
52
+ { :params => body }
53
+ end
54
+ end
16
55
  options[:params] = Appsignal::Utils::HashSanitizer.sanitize(
17
56
  args,
18
57
  Appsignal.config[:filter_parameters]
19
58
  )
20
59
 
21
- if sqs_msg.attributes.key?("SentTimestamp")
22
- options[:queue_start] = Time.at(sqs_msg.attributes["SentTimestamp"].to_i / 1000)
60
+ if attributes.key?("SentTimestamp")
61
+ options[:queue_start] = Time.at(attributes["SentTimestamp"].to_i / 1000)
23
62
  end
24
63
 
25
64
  Appsignal.monitor_transaction("perform_job.shoryuken", options) do
@@ -11,6 +11,7 @@ module Appsignal
11
11
  BLANK = "".freeze
12
12
  ALLOWED_TAG_KEY_TYPES = [Symbol, String].freeze
13
13
  ALLOWED_TAG_VALUE_TYPES = [Symbol, String, Integer].freeze
14
+ BREADCRUMB_LIMIT = 20
14
15
 
15
16
  class << self
16
17
  def create(id, namespace, request, options = {})
@@ -58,7 +59,7 @@ module Appsignal
58
59
  end
59
60
  end
60
61
 
61
- attr_reader :ext, :transaction_id, :action, :namespace, :request, :paused, :tags, :options, :discarded
62
+ attr_reader :ext, :transaction_id, :action, :namespace, :request, :paused, :tags, :options, :discarded, :breadcrumbs
62
63
 
63
64
  # @!attribute params
64
65
  # Attribute for parameters of the transaction.
@@ -80,6 +81,7 @@ module Appsignal
80
81
  @paused = false
81
82
  @discarded = false
82
83
  @tags = {}
84
+ @breadcrumbs = []
83
85
  @store = Hash.new({})
84
86
  @options = options
85
87
  @options[:params_method] ||= :params
@@ -156,6 +158,31 @@ module Appsignal
156
158
  @tags.merge!(given_tags)
157
159
  end
158
160
 
161
+ # Add breadcrumbs to the transaction.
162
+ #
163
+ # @param category [String] category of breadcrumb
164
+ # e.g. "UI", "Network", "Navigation", "Console".
165
+ # @param action [String] name of breadcrumb
166
+ # e.g "The user clicked a button", "HTTP 500 from http://blablabla.com"
167
+ # @option message [String] optional message in string format
168
+ # @option metadata [Hash<String,String>] key/value metadata in <string, string> format
169
+ # @option time [Time] time of breadcrumb, should respond to `.to_i` defaults to `Time.now.utc`
170
+ # @return [void]
171
+ #
172
+ # @see Appsignal.add_breadcrumb
173
+ # @see http://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html
174
+ # Breadcrumb reference
175
+ def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.now.utc)
176
+ @breadcrumbs.push(
177
+ :time => time.to_i,
178
+ :category => category,
179
+ :action => action,
180
+ :message => message,
181
+ :metadata => metadata
182
+ )
183
+ @breadcrumbs = @breadcrumbs.last(BREADCRUMB_LIMIT)
184
+ end
185
+
159
186
  # Set an action name for the transaction.
160
187
  #
161
188
  # An action name is used to identify the location of a certain sample;
@@ -287,7 +314,8 @@ module Appsignal
287
314
  :environment => sanitized_environment,
288
315
  :session_data => sanitized_session_data,
289
316
  :metadata => metadata,
290
- :tags => sanitized_tags
317
+ :tags => sanitized_tags,
318
+ :breadcrumbs => breadcrumbs
291
319
  }.each do |key, data|
292
320
  set_sample_data(key, data)
293
321
  end