appsignal 2.8.4 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop_todo.yml +7 -16
  4. data/.travis.yml +4 -1
  5. data/CHANGELOG.md +16 -0
  6. data/README.md +23 -0
  7. data/Rakefile +10 -7
  8. data/appsignal.gemspec +3 -0
  9. data/build_matrix.yml +5 -1
  10. data/ext/Rakefile +23 -16
  11. data/ext/agent.yml +37 -37
  12. data/ext/base.rb +86 -24
  13. data/ext/extconf.rb +33 -26
  14. data/gemfiles/rails-6.0.gemfile +5 -0
  15. data/lib/appsignal.rb +14 -489
  16. data/lib/appsignal/cli/diagnose.rb +84 -4
  17. data/lib/appsignal/cli/diagnose/paths.rb +0 -5
  18. data/lib/appsignal/cli/diagnose/utils.rb +17 -0
  19. data/lib/appsignal/cli/helpers.rb +6 -0
  20. data/lib/appsignal/cli/install.rb +13 -7
  21. data/lib/appsignal/config.rb +1 -2
  22. data/lib/appsignal/event_formatter.rb +4 -5
  23. data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
  24. data/lib/appsignal/extension.rb +2 -2
  25. data/lib/appsignal/helpers/instrumentation.rb +485 -0
  26. data/lib/appsignal/helpers/metrics.rb +55 -0
  27. data/lib/appsignal/hooks.rb +9 -8
  28. data/lib/appsignal/hooks/puma.rb +65 -9
  29. data/lib/appsignal/hooks/sidekiq.rb +90 -0
  30. data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
  31. data/lib/appsignal/integrations/railtie.rb +2 -1
  32. data/lib/appsignal/marker.rb +2 -3
  33. data/lib/appsignal/minutely.rb +164 -14
  34. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
  35. data/lib/appsignal/system.rb +16 -18
  36. data/lib/appsignal/utils/rails_helper.rb +16 -0
  37. data/lib/appsignal/version.rb +1 -1
  38. data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
  39. data/spec/lib/appsignal/cli/install_spec.rb +6 -1
  40. data/spec/lib/appsignal/config_spec.rb +3 -3
  41. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
  42. data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
  43. data/spec/lib/appsignal/hooks/puma_spec.rb +129 -0
  44. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +147 -0
  45. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
  46. data/spec/lib/appsignal/minutely_spec.rb +251 -21
  47. data/spec/lib/appsignal/system_spec.rb +0 -35
  48. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
  49. data/spec/lib/appsignal/utils/json_spec.rb +7 -3
  50. data/spec/lib/appsignal_spec.rb +27 -2
  51. data/spec/spec_helper.rb +13 -0
  52. data/spec/support/helpers/log_helpers.rb +6 -0
  53. data/spec/support/project_fixture/config/appsignal.yml +1 -0
  54. data/spec/support/stubs/sidekiq/api.rb +4 -0
  55. metadata +8 -2
@@ -82,6 +82,8 @@ module Appsignal
82
82
  print_empty_line
83
83
 
84
84
  library_information
85
+ data[:installation] = fetch_installation_report
86
+ print_installation_report
85
87
  print_empty_line
86
88
 
87
89
  host_information
@@ -180,7 +182,8 @@ module Appsignal
180
182
  if rails_app?
181
183
  data[:app][:rails] = true
182
184
  current_path = Rails.root
183
- initial_config[:name] = Rails.application.class.parent_name
185
+ initial_config[:name] =
186
+ Appsignal::Utils::RailsHelper.detected_rails_app_name
184
187
  initial_config[:log_path] = current_path.join("log")
185
188
  end
186
189
 
@@ -325,12 +328,89 @@ module Appsignal
325
328
  save :language, "ruby"
326
329
  puts_and_save :package_version, "Gem version", Appsignal::VERSION
327
330
  puts_and_save :agent_version, "Agent version", Appsignal::Extension.agent_version
328
- puts_and_save :agent_architecture, "Agent architecture",
329
- Appsignal::System.installed_agent_architecture
330
331
  puts_and_save :extension_loaded, "Extension loaded", Appsignal.extension_loaded
331
332
  end
332
333
  end
333
334
 
335
+ def fetch_installation_report
336
+ path = File.expand_path("../../../../ext/install.report", __FILE__)
337
+ raw_report = File.read(path)
338
+ Utils.parse_yaml(raw_report)
339
+ rescue => e
340
+ {
341
+ "parsing_error" => {
342
+ "error" => "#{e.class}: #{e}",
343
+ "backtrace" => e.backtrace
344
+ }.tap do |r|
345
+ r["raw"] = raw_report if raw_report
346
+ end
347
+ }
348
+ end
349
+
350
+ def print_installation_report
351
+ puts "\nExtension installation report"
352
+ install_report = data[:installation]
353
+ if install_report.key? "parsing_error"
354
+ print_installation_report_parsing_error(install_report)
355
+ return
356
+ end
357
+
358
+ print_installation_result_report(install_report)
359
+ print_installation_language_report(install_report)
360
+ print_installation_download_report(install_report)
361
+ print_installation_build_report(install_report)
362
+ print_installation_host_report(install_report)
363
+ end
364
+
365
+ def print_installation_report_parsing_error(report)
366
+ report = report["parsing_error"]
367
+ puts " Error found while parsing the report."
368
+ puts " Error: #{report["error"]}"
369
+ puts " Raw report:\n#{report["raw"]}" if report["raw"]
370
+ end
371
+
372
+ def print_installation_result_report(report)
373
+ report = report.fetch("download", {})
374
+ puts " Installation result"
375
+ puts " Status: #{report["status"]}"
376
+ puts " Message: #{report["message"]}" if report["message"]
377
+ puts " Error: #{report["error"]}" if report["error"]
378
+ end
379
+
380
+ def print_installation_language_report(report)
381
+ report = report.fetch("language", {})
382
+ puts " Language details"
383
+ puts " Implementation: #{report["implementation"]}"
384
+ puts " Ruby version: #{report["version"]}"
385
+ end
386
+
387
+ def print_installation_download_report(report)
388
+ report = report.fetch("download", {})
389
+ puts " Download details"
390
+ puts " Download URL: #{report["download_url"]}"
391
+ puts " Checksum: #{report["checksum"]}"
392
+ end
393
+
394
+ def print_installation_build_report(report)
395
+ report = report.fetch("build", {})
396
+ puts " Build details"
397
+ puts " Install time: #{report["time"]}"
398
+ puts " Architecture: #{report["architecture"]}"
399
+ puts " Target: #{report["target"]}"
400
+ puts " Musl override: #{report["musl_override"]}"
401
+ puts " Library type: #{report["library_type"]}"
402
+ puts " Source: #{report["source"]}" if report["source"] != "remote"
403
+ puts " Dependencies: #{report["dependencies"]}"
404
+ puts " Flags: #{report["flags"]}"
405
+ end
406
+
407
+ def print_installation_host_report(report)
408
+ report = report.fetch("host", {})
409
+ puts " Host details"
410
+ puts " Root user: #{report["root_user"]}"
411
+ puts " Dependencies: #{report["dependencies"]}"
412
+ end
413
+
334
414
  def host_information
335
415
  rbconfig = RbConfig::CONFIG
336
416
  puts "Host information"
@@ -349,7 +429,7 @@ module Appsignal
349
429
  save :heroku, Appsignal::System.heroku?
350
430
 
351
431
  save :root, Process.uid.zero?
352
- puts_value "root user",
432
+ puts_value "Root user",
353
433
  Process.uid.zero? ? "true (not recommended)" : "false"
354
434
  puts_and_save :running_in_container, "Running in container",
355
435
  Appsignal::Extension.running_in_container?
@@ -17,7 +17,6 @@ module Appsignal
17
17
  begin
18
18
  config = Appsignal.config
19
19
  log_file_path = config.log_file_path
20
- install_log_path = File.join("ext", "install.log")
21
20
  makefile_log_path = File.join("ext", "mkmf.log")
22
21
  {
23
22
  :package_install_path => {
@@ -36,10 +35,6 @@ module Appsignal
36
35
  :label => "Log directory",
37
36
  :path => log_file_path ? File.dirname(log_file_path) : ""
38
37
  },
39
- install_log_path => {
40
- :label => "Extension install log",
41
- :path => File.join(gem_path, install_log_path)
42
- },
43
38
  makefile_log_path => {
44
39
  :label => "Makefile install log",
45
40
  :path => File.join(gem_path, makefile_log_path)
@@ -30,6 +30,23 @@ module Appsignal
30
30
 
31
31
  IO.binread(path, length, offset)
32
32
  end
33
+
34
+ def self.parse_yaml(contents)
35
+ arguments = [contents]
36
+ if YAML.respond_to? :safe_load
37
+ method = :safe_load
38
+ arguments << \
39
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
40
+ # Use keyword params for Ruby 2.6 and up
41
+ { :permitted_classes => [Time] }
42
+ else
43
+ [Time]
44
+ end
45
+ else
46
+ method = :load
47
+ end
48
+ YAML.send(method, *arguments)
49
+ end
33
50
  end
34
51
  end
35
52
  end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "appsignal/utils/rails_helper"
4
+
3
5
  module Appsignal
4
6
  class CLI
5
7
  module Helpers
6
8
  private
7
9
 
10
+ def ruby_2_6_or_up?
11
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
12
+ end
13
+
8
14
  def colorize(text, color)
9
15
  return text if Gem.win_platform?
10
16
 
@@ -77,8 +77,7 @@ module Appsignal
77
77
 
78
78
  require File.expand_path(File.join(Dir.pwd, "config/application.rb"))
79
79
 
80
- config[:name] = Rails.application.class.parent_name
81
-
80
+ config[:name] = Appsignal::Utils::RailsHelper.detected_rails_app_name
82
81
  name_overwritten = yes_or_no(" Your app's name is: '#{config[:name]}' \n Do you want to change how this is displayed in AppSignal? (y/n): ")
83
82
  puts
84
83
  if name_overwritten
@@ -249,12 +248,19 @@ module Appsignal
249
248
  end
250
249
 
251
250
  def write_config_file(data)
252
- template = ERB.new(
253
- File.read(File.join(File.dirname(__FILE__), "../../../resources/appsignal.yml.erb")),
254
- nil,
255
- "-"
251
+ filename = File.join(
252
+ File.dirname(__FILE__),
253
+ "../../../resources/appsignal.yml.erb"
256
254
  )
257
-
255
+ file_contents = File.read(filename)
256
+ arguments = [file_contents]
257
+ if ruby_2_6_or_up?
258
+ arguments << { :trim_mode => "-" }
259
+ else
260
+ arguments << nil
261
+ arguments << "-"
262
+ end
263
+ template = ERB.new(*arguments)
258
264
  config = template.result(OpenStruct.new(data).instance_eval { binding })
259
265
 
260
266
  FileUtils.mkdir_p(File.join(Dir.pwd, "config"))
@@ -36,7 +36,7 @@ module Appsignal
36
36
  :enable_allocation_tracking => true,
37
37
  :enable_gc_instrumentation => false,
38
38
  :enable_host_metrics => true,
39
- :enable_minutely_probes => false,
39
+ :enable_minutely_probes => true,
40
40
  :ca_file_path => File.expand_path(File.join("../../../resources/cacert.pem"), __FILE__),
41
41
  :dns_servers => [],
42
42
  :files_world_accessible => true
@@ -214,7 +214,6 @@ module Appsignal
214
214
  ENV["_APPSIGNAL_WORKING_DIR_PATH"] = config_hash[:working_dir_path] if config_hash[:working_dir_path]
215
215
  ENV["_APPSIGNAL_WORKING_DIRECTORY_PATH"] = config_hash[:working_directory_path] if config_hash[:working_directory_path]
216
216
  ENV["_APPSIGNAL_ENABLE_HOST_METRICS"] = config_hash[:enable_host_metrics].to_s
217
- ENV["_APPSIGNAL_ENABLE_MINUTELY_PROBES"] = config_hash[:enable_minutely_probes].to_s
218
217
  ENV["_APPSIGNAL_HOSTNAME"] = config_hash[:hostname].to_s
219
218
  ENV["_APPSIGNAL_PROCESS_NAME"] = $PROGRAM_NAME
220
219
  ENV["_APPSIGNAL_CA_FILE_PATH"] = config_hash[:ca_file_path].to_s
@@ -75,16 +75,15 @@ module Appsignal
75
75
 
76
76
  def initialize_formatter(name, formatter)
77
77
  format_method = formatter.instance_method(:format)
78
- if format_method && format_method.arity == 1
79
- formatter_classes[name] = formatter
80
- formatters[name] = formatter.new
81
- else
78
+ if !format_method || format_method.arity != 1
82
79
  raise "#{formatter} does not have a format(payload) method"
83
80
  end
81
+ formatter_classes[name] = formatter
82
+ formatters[name] = formatter.new
84
83
  rescue => ex
85
84
  formatter_classes.delete(name)
86
85
  formatters.delete(name)
87
- logger.warn("'#{ex.message}' when initializing #{name} event formatter")
86
+ logger.error("'#{ex.message}' when initializing #{name} event formatter")
88
87
  end
89
88
 
90
89
  def register_deprecated_formatter(name)
@@ -6,65 +6,66 @@ module Appsignal
6
6
  module Moped
7
7
  class QueryFormatter
8
8
  def format(payload)
9
- if payload[:ops] && !payload[:ops].empty?
10
- op = payload[:ops].first
11
- case op.class.to_s
12
- when "Moped::Protocol::Command"
13
- [
14
- "Command", {
15
- :database => op.full_collection_name,
16
- :selector => sanitize(op.selector, true, :mongodb)
17
- }.inspect
18
- ]
19
- when "Moped::Protocol::Query"
20
- [
21
- "Query", {
22
- :database => op.full_collection_name,
23
- :selector => sanitize(op.selector, false, :mongodb),
24
- :flags => op.flags,
25
- :limit => op.limit,
26
- :skip => op.skip,
27
- :fields => op.fields
28
- }.inspect
29
- ]
30
- when "Moped::Protocol::Delete"
31
- [
32
- "Delete", {
33
- :database => op.full_collection_name,
34
- :selector => sanitize(op.selector, false, :mongodb),
35
- :flags => op.flags
36
- }.inspect
37
- ]
38
- when "Moped::Protocol::Insert"
39
- [
40
- "Insert", {
41
- :database => op.full_collection_name,
42
- :documents => sanitize(op.documents, true, :mongodb),
43
- :count => op.documents.count,
44
- :flags => op.flags
45
- }.inspect
46
- ]
47
- when "Moped::Protocol::Update"
48
- [
49
- "Update",
50
- {
51
- :database => op.full_collection_name,
52
- :selector => sanitize(op.selector, false, :mongodb),
53
- :update => sanitize(op.update, true, :mongodb),
54
- :flags => op.flags
55
- }.inspect
56
- ]
57
- when "Moped::Protocol::KillCursors"
58
- [
59
- "KillCursors",
60
- { :number_of_cursor_ids => op.number_of_cursor_ids }.inspect
61
- ]
62
- else
63
- [
64
- op.class.to_s.sub("Moped::Protocol::", ""),
65
- { :database => op.full_collection_name }.inspect
66
- ]
67
- end
9
+ return unless payload[:ops]
10
+ return if payload[:ops].empty?
11
+
12
+ op = payload[:ops].first
13
+ case op.class.to_s
14
+ when "Moped::Protocol::Command"
15
+ [
16
+ "Command", {
17
+ :database => op.full_collection_name,
18
+ :selector => sanitize(op.selector, true, :mongodb)
19
+ }.inspect
20
+ ]
21
+ when "Moped::Protocol::Query"
22
+ [
23
+ "Query", {
24
+ :database => op.full_collection_name,
25
+ :selector => sanitize(op.selector, false, :mongodb),
26
+ :flags => op.flags,
27
+ :limit => op.limit,
28
+ :skip => op.skip,
29
+ :fields => op.fields
30
+ }.inspect
31
+ ]
32
+ when "Moped::Protocol::Delete"
33
+ [
34
+ "Delete", {
35
+ :database => op.full_collection_name,
36
+ :selector => sanitize(op.selector, false, :mongodb),
37
+ :flags => op.flags
38
+ }.inspect
39
+ ]
40
+ when "Moped::Protocol::Insert"
41
+ [
42
+ "Insert", {
43
+ :database => op.full_collection_name,
44
+ :documents => sanitize(op.documents, true, :mongodb),
45
+ :count => op.documents.count,
46
+ :flags => op.flags
47
+ }.inspect
48
+ ]
49
+ when "Moped::Protocol::Update"
50
+ [
51
+ "Update",
52
+ {
53
+ :database => op.full_collection_name,
54
+ :selector => sanitize(op.selector, false, :mongodb),
55
+ :update => sanitize(op.update, true, :mongodb),
56
+ :flags => op.flags
57
+ }.inspect
58
+ ]
59
+ when "Moped::Protocol::KillCursors"
60
+ [
61
+ "KillCursors",
62
+ { :number_of_cursor_ids => op.number_of_cursor_ids }.inspect
63
+ ]
64
+ else
65
+ [
66
+ op.class.to_s.sub("Moped::Protocol::", ""),
67
+ { :database => op.full_collection_name }.inspect
68
+ ]
68
69
  end
69
70
  end
70
71
 
@@ -12,8 +12,8 @@ begin
12
12
  end
13
13
  rescue LoadError => err
14
14
  Appsignal.logger.error(
15
- "Failed to load extension (#{err}), please check the install.log file in " \
16
- "the ext directory of the gem and email us at support@appsignal.com"
15
+ "Failed to load extension (#{err}), please run `appsignal diagnose` " \
16
+ "and email us at support@appsignal.com"
17
17
  )
18
18
  Appsignal.extension_loaded = false
19
19
  end
@@ -0,0 +1,485 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Helpers
5
+ # @api private
6
+ module Instrumentation # rubocop:disable Metrics/ModuleLength
7
+ # Creates an AppSignal transaction for the given block.
8
+ #
9
+ # If AppSignal is not {.active?} it will still execute the block, but not
10
+ # create a transaction for it.
11
+ #
12
+ # A event is created for this transaction with the name given in the
13
+ # `name` argument. The event name must start with either `perform_job` or
14
+ # `process_action` to differentiate between the "web" and "background"
15
+ # namespace. Custom namespaces are not supported by this helper method.
16
+ #
17
+ # This helper method also captures any exception that occurs in the given
18
+ # block.
19
+ #
20
+ # @example
21
+ # Appsignal.monitor_transaction("perform_job.nightly_update") do
22
+ # # your code
23
+ # end
24
+ #
25
+ # @example with an environment
26
+ # Appsignal.monitor_transaction(
27
+ # "perform_job.nightly_update",
28
+ # :metadata => { "user_id" => 1 }
29
+ # ) do
30
+ # # your code
31
+ # end
32
+ #
33
+ # @param name [String] main event name.
34
+ # @param env [Hash<Symbol, Object>]
35
+ # @option env [Hash<Symbol/String, Object>] :params Params for the
36
+ # monitored request/job, see {Appsignal::Transaction#params=} for more
37
+ # information.
38
+ # @option env [String] :controller name of the controller in which the
39
+ # transaction was recorded.
40
+ # @option env [String] :class name of the Ruby class in which the
41
+ # transaction was recorded. If `:controller` is also given,
42
+ # `:controller` is used instead.
43
+ # @option env [String] :action name of the controller action in which the
44
+ # transaction was recorded.
45
+ # @option env [String] :method name of the Ruby method in which the
46
+ # transaction was recorded. If `:action` is also given, `:action`
47
+ # is used instead.
48
+ # @option env [Integer] :queue_start the moment the request/job was
49
+ # queued. Used to track how long requests/jobs were queued before being
50
+ # executed.
51
+ # @option env [Hash<Symbol/String, String/Fixnum>] :metadata Additional
52
+ # metadata for the transaction, see
53
+ # {Appsignal::Transaction#set_metadata} for more information.
54
+ # @yield the block to monitor.
55
+ # @raise [Exception] any exception that occurs within the given block is
56
+ # re-raised by this method.
57
+ # @return [Object] the value of the given block is returned.
58
+ # @since 0.10.0
59
+ def monitor_transaction(name, env = {})
60
+ return yield unless active?
61
+
62
+ if name.start_with?("perform_job".freeze)
63
+ namespace = Appsignal::Transaction::BACKGROUND_JOB
64
+ request = Appsignal::Transaction::GenericRequest.new(env)
65
+ elsif name.start_with?("process_action".freeze)
66
+ namespace = Appsignal::Transaction::HTTP_REQUEST
67
+ request = ::Rack::Request.new(env)
68
+ else
69
+ logger.error("Unrecognized name '#{name}'")
70
+ return
71
+ end
72
+ transaction = Appsignal::Transaction.create(
73
+ SecureRandom.uuid,
74
+ namespace,
75
+ request
76
+ )
77
+ begin
78
+ Appsignal.instrument(name) do
79
+ yield
80
+ end
81
+ rescue Exception => error # rubocop:disable Lint/RescueException
82
+ transaction.set_error(error)
83
+ raise error
84
+ ensure
85
+ transaction.set_http_or_background_action(request.env)
86
+ transaction.set_http_or_background_queue_start
87
+ Appsignal::Transaction.complete_current!
88
+ end
89
+ end
90
+
91
+ # Monitor a transaction, stop AppSignal and wait for this single
92
+ # transaction to be flushed.
93
+ #
94
+ # Useful for cases such as Rake tasks and Resque-like systems where a
95
+ # process is forked and immediately exits after the transaction finishes.
96
+ #
97
+ # @see monitor_transaction
98
+ def monitor_single_transaction(name, env = {}, &block)
99
+ monitor_transaction(name, env, &block)
100
+ ensure
101
+ stop("monitor_single_transaction")
102
+ end
103
+
104
+ # Listen for an error to occur and send it to AppSignal.
105
+ #
106
+ # Uses {.send_error} to directly send the error in a separate
107
+ # transaction. Does not add the error to the current transaction.
108
+ #
109
+ # Make sure that AppSignal is integrated in your application beforehand.
110
+ # AppSignal won't record errors unless {Config#active?} is `true`.
111
+ #
112
+ # @example
113
+ # # my_app.rb
114
+ # # setup AppSignal beforehand
115
+ #
116
+ # Appsignal.listen_for_error do
117
+ # # my code
118
+ # raise "foo"
119
+ # end
120
+ #
121
+ # @see Transaction.set_tags
122
+ # @see Transaction.set_namespace
123
+ # @see .send_error
124
+ # @see https://docs.appsignal.com/ruby/instrumentation/integrating-appsignal.html
125
+ # AppSignal integration guide
126
+ #
127
+ # @param tags [Hash, nil]
128
+ # @param namespace [String] the namespace for this error.
129
+ # @yield yields the given block.
130
+ # @return [Object] returns the return value of the given block.
131
+ def listen_for_error(
132
+ tags = nil,
133
+ namespace = Appsignal::Transaction::HTTP_REQUEST
134
+ )
135
+ yield
136
+ rescue Exception => error # rubocop:disable Lint/RescueException
137
+ send_error(error, tags, namespace)
138
+ raise error
139
+ end
140
+ alias :listen_for_exception :listen_for_error
141
+
142
+ # Send an error to AppSignal regardless of the context.
143
+ #
144
+ # Records and send the exception to AppSignal.
145
+ #
146
+ # This instrumentation helper does not require a transaction to be
147
+ # active, it starts a new transaction by itself.
148
+ #
149
+ # Use {.set_error} if your want to add an exception to the current
150
+ # transaction.
151
+ #
152
+ # **Note**: Does not do anything if AppSignal is not active or when the
153
+ # "error" is not a class extended from Ruby's Exception class.
154
+ #
155
+ # @example Send an exception
156
+ # begin
157
+ # raise "oh no!"
158
+ # rescue => e
159
+ # Appsignal.send_error(e)
160
+ # end
161
+ #
162
+ # @example Send an exception with tags
163
+ # begin
164
+ # raise "oh no!"
165
+ # rescue => e
166
+ # Appsignal.send_error(e, :key => "value")
167
+ # end
168
+ #
169
+ # @example Add more metadata to transaction
170
+ # Appsignal.send_error(e, :key => "value") do |transaction|
171
+ # transaction.params(:search_query => params[:search_query])
172
+ # transaction.set_action("my_action_name")
173
+ # transaction.set_namespace("my_namespace")
174
+ # end
175
+ #
176
+ # @param error [Exception] The error to send to AppSignal.
177
+ # @param tags [Hash{String, Symbol => String, Symbol, Integer}]
178
+ # Additional tags to add to the error. See also {.tag_request}.
179
+ # @param namespace [String] The namespace in which the error occurred.
180
+ # See also {.set_namespace}.
181
+ # @yield [transaction] yields block to allow modification of the
182
+ # transaction before it's send.
183
+ # @yieldparam transaction [Transaction] yields the AppSignal transaction
184
+ # used to send the error.
185
+ # @return [void]
186
+ #
187
+ # @see http://docs.appsignal.com/ruby/instrumentation/exception-handling.html
188
+ # Exception handling guide
189
+ # @see http://docs.appsignal.com/ruby/instrumentation/tagging.html
190
+ # Tagging guide
191
+ # @since 0.6.0
192
+ def send_error(
193
+ error,
194
+ tags = nil,
195
+ namespace = Appsignal::Transaction::HTTP_REQUEST
196
+ )
197
+ return unless active?
198
+ unless error.is_a?(Exception)
199
+ logger.error("Can't send error, given value is not an exception")
200
+ return
201
+ end
202
+ transaction = Appsignal::Transaction.new(
203
+ SecureRandom.uuid,
204
+ namespace,
205
+ Appsignal::Transaction::GenericRequest.new({})
206
+ )
207
+ transaction.set_tags(tags) if tags
208
+ transaction.set_error(error)
209
+ yield transaction if block_given?
210
+ transaction.complete
211
+ end
212
+ alias :send_exception :send_error
213
+
214
+ # Set an error on the current transaction.
215
+ #
216
+ # **Note**: Does not do anything if AppSignal is not active, no
217
+ # transaction is currently active or when the "error" is not a class
218
+ # extended from Ruby's Exception class.
219
+ #
220
+ # @example Manual instrumentation of set_error.
221
+ # # Manually starting AppSignal here
222
+ # # Manually starting a transaction here.
223
+ # begin
224
+ # raise "oh no!"
225
+ # rescue => e
226
+ # Appsignal.set_error(error)
227
+ # end
228
+ # # Manually completing the transaction here.
229
+ # # Manually stopping AppSignal here
230
+ #
231
+ # @example In a Rails application
232
+ # class SomeController < ApplicationController
233
+ # # The AppSignal transaction is created by our integration for you.
234
+ # def create
235
+ # # Do something that breaks
236
+ # rescue => e
237
+ # Appsignal.set_error(e)
238
+ # end
239
+ # end
240
+ #
241
+ # @param exception [Exception] The error to add to the current
242
+ # transaction.
243
+ # @param tags [Hash{String, Symbol => String, Symbol, Integer}]
244
+ # Additional tags to add to the error. See also {.tag_request}.
245
+ # @param namespace [String] The namespace in which the error occurred.
246
+ # See also {.set_namespace}.
247
+ # @return [void]
248
+ #
249
+ # @see Transaction#set_error
250
+ # @see http://docs.appsignal.com/ruby/instrumentation/exception-handling.html
251
+ # Exception handling guide
252
+ # @since 0.6.6
253
+ def set_error(exception, tags = nil, namespace = nil)
254
+ return if !active? ||
255
+ Appsignal::Transaction.current.nil? ||
256
+ exception.nil?
257
+ transaction = Appsignal::Transaction.current
258
+ transaction.set_error(exception)
259
+ transaction.set_tags(tags) if tags
260
+ transaction.set_namespace(namespace) if namespace
261
+ end
262
+ alias :set_exception :set_error
263
+ alias :add_exception :set_error
264
+
265
+ # Set a custom action name for the current transaction.
266
+ #
267
+ # When using an integration such as the Rails or Sinatra AppSignal will
268
+ # try to find the action name from the controller or endpoint for you.
269
+ #
270
+ # If you want to customize the action name as it appears on AppSignal.com
271
+ # you can use this method. This overrides the action name AppSignal
272
+ # generates in an integration.
273
+ #
274
+ # @example in a Rails controller
275
+ # class SomeController < ApplicationController
276
+ # before_action :set_appsignal_action
277
+ #
278
+ # def set_appsignal_action
279
+ # Appsignal.set_action("DynamicController#dynamic_method")
280
+ # end
281
+ # end
282
+ #
283
+ # @param action [String]
284
+ # @return [void]
285
+ # @see Transaction#set_action
286
+ # @since 2.2.0
287
+ def set_action(action)
288
+ return if !active? ||
289
+ Appsignal::Transaction.current.nil? ||
290
+ action.nil?
291
+ Appsignal::Transaction.current.set_action(action)
292
+ end
293
+
294
+ # Set a custom namespace for the current transaction.
295
+ #
296
+ # When using an integration such as Rails or Sidekiq AppSignal will try
297
+ # to find a appropriate namespace for the transaction.
298
+ #
299
+ # A Rails controller will be automatically put in the "http_request"
300
+ # namespace, while a Sidekiq background job is put in the
301
+ # "background_job" namespace.
302
+ #
303
+ # Note: The "http_request" namespace gets transformed on AppSignal.com to
304
+ # "Web" and "background_job" gets transformed to "Background".
305
+ #
306
+ # If you want to customize the namespace in which transactions appear you
307
+ # can use this method. This overrides the namespace AppSignal uses by
308
+ # default.
309
+ #
310
+ # A common request we've seen is to split the administration panel from
311
+ # the main application.
312
+ #
313
+ # @example create a custom admin namespace
314
+ # class AdminController < ApplicationController
315
+ # before_action :set_appsignal_namespace
316
+ #
317
+ # def set_appsignal_namespace
318
+ # Appsignal.set_namespace("admin")
319
+ # end
320
+ # end
321
+ #
322
+ # @param namespace [String]
323
+ # @return [void]
324
+ # @see Transaction#set_namespace
325
+ # @since 2.2.0
326
+ def set_namespace(namespace)
327
+ return if !active? ||
328
+ Appsignal::Transaction.current.nil? ||
329
+ namespace.nil?
330
+ Appsignal::Transaction.current.set_namespace(namespace)
331
+ end
332
+
333
+ # Set tags on the current transaction.
334
+ #
335
+ # Tags are extra bits of information that are added to transaction and
336
+ # appear on sample details pages on AppSignal.com.
337
+ #
338
+ # @example
339
+ # Appsignal.tag_request(:locale => "en")
340
+ # Appsignal.tag_request("locale" => "en")
341
+ # Appsignal.tag_request("user_id" => 1)
342
+ #
343
+ # @example Nested hashes are not supported
344
+ # # Bad
345
+ # Appsignal.tag_request(:user => { :locale => "en" })
346
+ #
347
+ # @example in a Rails controller
348
+ # class SomeController < ApplicationController
349
+ # before_action :set_appsignal_tags
350
+ #
351
+ # def set_appsignal_tags
352
+ # Appsignal.tag_request(:locale => I18n.locale)
353
+ # end
354
+ # end
355
+ #
356
+ # @param tags [Hash] Collection of tags.
357
+ # @option tags [String, Symbol, Integer] :any
358
+ # The name of the tag as a Symbol.
359
+ # @option tags [String, Symbol, Integer] "any"
360
+ # The name of the tag as a String.
361
+ # @return [void]
362
+ #
363
+ # @see Transaction.set_tags
364
+ # @see http://docs.appsignal.com/ruby/instrumentation/tagging.html
365
+ # Tagging guide
366
+ def tag_request(tags = {})
367
+ return unless active?
368
+ transaction = Appsignal::Transaction.current
369
+ return false unless transaction
370
+ transaction.set_tags(tags)
371
+ end
372
+ alias :tag_job :tag_request
373
+
374
+ # Instrument helper for AppSignal.
375
+ #
376
+ # For more help, read our custom instrumentation guide, listed under "See
377
+ # also".
378
+ #
379
+ # @example Simple instrumentation
380
+ # Appsignal.instrument("fetch.issue_fetcher") do
381
+ # # To be instrumented code
382
+ # end
383
+ #
384
+ # @example Instrumentation with title and body
385
+ # Appsignal.instrument(
386
+ # "fetch.issue_fetcher",
387
+ # "Fetching issue",
388
+ # "GitHub API"
389
+ # ) do
390
+ # # To be instrumented code
391
+ # end
392
+ #
393
+ # @param name [String] Name of the instrumented event. Read our event
394
+ # naming guide listed under "See also".
395
+ # @param title [String, nil] Human readable name of the event.
396
+ # @param body [String, nil] Value of importance for the event, such as
397
+ # the server against an API call is made.
398
+ # @param body_format [Integer] Enum for the type of event that is
399
+ # instrumented. Accepted values are {EventFormatter::DEFAULT} and
400
+ # {EventFormatter::SQL_BODY_FORMAT}, but we recommend you use
401
+ # {.instrument_sql} instead of {EventFormatter::SQL_BODY_FORMAT}.
402
+ # @yield yields the given block of code instrumented in an AppSignal
403
+ # event.
404
+ # @return [Object] Returns the block's return value.
405
+ #
406
+ # @see Appsignal::Transaction#instrument
407
+ # @see .instrument_sql
408
+ # @see http://docs.appsignal.com/ruby/instrumentation/instrumentation.html
409
+ # AppSignal custom instrumentation guide
410
+ # @see http://docs.appsignal.com/api/event-names.html
411
+ # AppSignal event naming guide
412
+ # @since 1.3.0
413
+ def instrument(
414
+ name,
415
+ title = nil,
416
+ body = nil,
417
+ body_format = Appsignal::EventFormatter::DEFAULT
418
+ )
419
+ Appsignal::Transaction.current.start_event
420
+ yield if block_given?
421
+ ensure
422
+ Appsignal::Transaction
423
+ .current
424
+ .finish_event(name, title, body, body_format)
425
+ end
426
+
427
+ # Instrumentation helper for SQL queries.
428
+ #
429
+ # This helper filters out values from SQL queries so you don't have to.
430
+ #
431
+ # @example SQL query instrumentation
432
+ # body = "SELECT * FROM ..."
433
+ # Appsignal.instrument_sql("perform.query", nil, body) do
434
+ # # To be instrumented code
435
+ # end
436
+ #
437
+ # @example SQL query instrumentation
438
+ # body = "WHERE email = 'foo@..'"
439
+ # Appsignal.instrument_sql("perform.query", nil, body) do
440
+ # # query value will replace 'foo..' with a question mark `?`.
441
+ # end
442
+ #
443
+ # @param name [String] Name of the instrumented event. Read our event
444
+ # naming guide listed under "See also".
445
+ # @param title [String, nil] Human readable name of the event.
446
+ # @param body [String, nil] SQL query that's being executed.
447
+ # @yield yields the given block of code instrumented in an AppSignal
448
+ # event.
449
+ # @return [Object] Returns the block's return value.
450
+ #
451
+ # @see .instrument
452
+ # @see http://docs.appsignal.com/ruby/instrumentation/instrumentation.html
453
+ # AppSignal custom instrumentation guide
454
+ # @see http://docs.appsignal.com/api/event-names.html
455
+ # AppSignal event naming guide
456
+ # @since 2.0.0
457
+ def instrument_sql(name, title = nil, body = nil, &block)
458
+ instrument(
459
+ name,
460
+ title,
461
+ body,
462
+ Appsignal::EventFormatter::SQL_BODY_FORMAT,
463
+ &block
464
+ )
465
+ end
466
+
467
+ # Convenience method for skipping instrumentations around a block of code.
468
+ #
469
+ # @example
470
+ # Appsignal.without_instrumentation do
471
+ # # Complex code here
472
+ # end
473
+ #
474
+ # @yield block of code that shouldn't be instrumented.
475
+ # @return [Object] Returns the return value of the block.
476
+ # @since 0.8.7
477
+ def without_instrumentation
478
+ Appsignal::Transaction.current.pause! if Appsignal::Transaction.current
479
+ yield
480
+ ensure
481
+ Appsignal::Transaction.current.resume! if Appsignal::Transaction.current
482
+ end
483
+ end
484
+ end
485
+ end