appsignal 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. data/.gitignore +19 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +30 -0
  4. data/Gemfile +3 -0
  5. data/LICENCE +20 -0
  6. data/README.md +48 -0
  7. data/Rakefile +52 -0
  8. data/appsignal.gemspec +33 -0
  9. data/bin/appsignal +13 -0
  10. data/config/appsignal.yml +8 -0
  11. data/gemfiles/3.0.gemfile +16 -0
  12. data/gemfiles/3.1.gemfile +16 -0
  13. data/gemfiles/3.2.gemfile +16 -0
  14. data/gemfiles/edge.gemfile +16 -0
  15. data/lib/appsignal.rb +45 -0
  16. data/lib/appsignal/agent.rb +104 -0
  17. data/lib/appsignal/auth_check.rb +19 -0
  18. data/lib/appsignal/capistrano.rb +41 -0
  19. data/lib/appsignal/cli.rb +118 -0
  20. data/lib/appsignal/config.rb +30 -0
  21. data/lib/appsignal/exception_notification.rb +25 -0
  22. data/lib/appsignal/marker.rb +35 -0
  23. data/lib/appsignal/middleware.rb +30 -0
  24. data/lib/appsignal/railtie.rb +19 -0
  25. data/lib/appsignal/transaction.rb +77 -0
  26. data/lib/appsignal/transaction/faulty_request_formatter.rb +30 -0
  27. data/lib/appsignal/transaction/params_sanitizer.rb +36 -0
  28. data/lib/appsignal/transaction/regular_request_formatter.rb +11 -0
  29. data/lib/appsignal/transaction/slow_request_formatter.rb +34 -0
  30. data/lib/appsignal/transaction/transaction_formatter.rb +93 -0
  31. data/lib/appsignal/transmitter.rb +53 -0
  32. data/lib/appsignal/version.rb +3 -0
  33. data/lib/generators/appsignal/USAGE +8 -0
  34. data/lib/generators/appsignal/appsignal_generator.rb +70 -0
  35. data/lib/generators/appsignal/templates/appsignal.yml +4 -0
  36. data/log/.gitkeep +0 -0
  37. data/resources/cacert.pem +3849 -0
  38. data/spec/appsignal/agent_spec.rb +259 -0
  39. data/spec/appsignal/auth_check_spec.rb +36 -0
  40. data/spec/appsignal/capistrano_spec.rb +81 -0
  41. data/spec/appsignal/cli_spec.rb +124 -0
  42. data/spec/appsignal/config_spec.rb +40 -0
  43. data/spec/appsignal/exception_notification_spec.rb +12 -0
  44. data/spec/appsignal/inactive_railtie_spec.rb +30 -0
  45. data/spec/appsignal/marker_spec.rb +83 -0
  46. data/spec/appsignal/middleware_spec.rb +73 -0
  47. data/spec/appsignal/railtie_spec.rb +54 -0
  48. data/spec/appsignal/transaction/faulty_request_formatter_spec.rb +49 -0
  49. data/spec/appsignal/transaction/params_sanitizer_spec.rb +68 -0
  50. data/spec/appsignal/transaction/regular_request_formatter_spec.rb +14 -0
  51. data/spec/appsignal/transaction/slow_request_formatter_spec.rb +76 -0
  52. data/spec/appsignal/transaction/transaction_formatter_spec.rb +178 -0
  53. data/spec/appsignal/transaction_spec.rb +191 -0
  54. data/spec/appsignal/transmitter_spec.rb +64 -0
  55. data/spec/appsignal_spec.rb +66 -0
  56. data/spec/generators/appsignal/appsignal_generator_spec.rb +222 -0
  57. data/spec/spec_helper.rb +85 -0
  58. data/spec/support/delegate_matcher.rb +39 -0
  59. metadata +247 -0
@@ -0,0 +1,19 @@
1
+ module Appsignal
2
+ class AuthCheck
3
+ delegate :uri, :to => :transmitter
4
+ attr_reader :config
5
+ attr_accessor :transmitter
6
+ ACTION = 'auth'
7
+
8
+ def initialize(environment)
9
+ @config = Appsignal::Config.new(Rails.root, environment).load
10
+ end
11
+
12
+ def perform
13
+ self.transmitter = Appsignal::Transmitter.new(
14
+ @config[:endpoint], ACTION, @config[:api_key]
15
+ )
16
+ transmitter.transmit({})
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ require 'capistrano'
2
+ require 'rails'
3
+ require 'appsignal/version'
4
+ require 'appsignal/config'
5
+ require 'appsignal/transmitter'
6
+ require 'appsignal/marker'
7
+
8
+ module Appsignal
9
+ class Capistrano
10
+ def self.tasks(config)
11
+ config.load do
12
+ after "deploy", "appsignal:deploy"
13
+ after "deploy:migrations", "appsignal:deploy"
14
+
15
+ namespace :appsignal do
16
+ task :deploy do
17
+ rails_env = fetch(:rails_env, 'production')
18
+ user = ENV['USER'] || ENV['USERNAME']
19
+
20
+ marker_data = {
21
+ :revision => current_revision,
22
+ :repository => repository,
23
+ :user => user
24
+ }
25
+
26
+ marker = Marker.new(marker_data, ENV['PWD'], rails_env, logger)
27
+ if config.dry_run
28
+ logger.info "Dry run: Deploy marker not actually sent."
29
+ else
30
+ marker.transmit
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ if Capistrano::Configuration.instance
40
+ Appsignal::Capistrano.tasks(Capistrano::Configuration.instance)
41
+ end
@@ -0,0 +1,118 @@
1
+ require 'optparse'
2
+ require 'logger'
3
+ require 'yaml'
4
+ require 'rails'
5
+ require 'appsignal/version'
6
+ require 'appsignal/config'
7
+ require 'appsignal/marker'
8
+ require 'appsignal/transmitter'
9
+
10
+ module Appsignal
11
+ class CLI
12
+ AVAILABLE_COMMANDS = %w( notify_of_deploy )
13
+
14
+ class << self
15
+ def run(argv=ARGV)
16
+ unless File.exists?(File.join(ENV['PWD'], 'config/appsignal.yml'))
17
+ puts 'No config file present at config/appsignal.yml'
18
+ puts 'Log in to https://appsignal.com to get instructions on how to generate the config file.'
19
+ exit(1)
20
+ end
21
+ options = {}
22
+ global = global_option_parser(options)
23
+ commands = command_option_parser(options)
24
+
25
+ global.order!(argv)
26
+ command = argv.shift
27
+ if command then
28
+ if AVAILABLE_COMMANDS.include?(command) then
29
+ commands[command].parse!(argv)
30
+ case options[:command]
31
+ when :notify_of_deploy
32
+ notify_of_deploy(options)
33
+ end
34
+ else
35
+ puts "Command '#{command}' does not exist, run appsignal -h to see the help"
36
+ exit(1)
37
+ end
38
+ else
39
+ # Print help
40
+ puts global
41
+ exit(0)
42
+ end
43
+ end
44
+
45
+ def logger
46
+ Logger.new($stdout)
47
+ end
48
+
49
+ def global_option_parser(options)
50
+ OptionParser.new do |o|
51
+ o.banner = %Q{Usage: appsignal <command> [options]}
52
+
53
+ o.on '-v', '--version', "Print version and exit" do |arg|
54
+ puts "Appsignal #{Appsignal::VERSION}"
55
+ exit(0)
56
+ end
57
+
58
+ o.on '-h', '--help', "Show help and exit" do
59
+ puts o
60
+ exit(0)
61
+ end
62
+
63
+ o.separator ''
64
+ o.separator "Available commands: #{AVAILABLE_COMMANDS.join(', ')}"
65
+ end
66
+ end
67
+
68
+ def command_option_parser(options)
69
+ {
70
+ 'notify_of_deploy' => OptionParser.new do |o|
71
+ o.banner = %Q{Usage: appsignal notify_of_deploy [options] }
72
+ options[:command] = :notify_of_deploy
73
+
74
+ o.on '--revision=<revision>', "The revision you're deploying" do |arg|
75
+ options[:revision] = arg
76
+ end
77
+
78
+ o.on '--repository=<repository>', "The location of the main code repository" do |arg|
79
+ options[:repository] = arg
80
+ end
81
+
82
+ o.on '--user=<user>', "The name of the user that's deploying" do |arg|
83
+ options[:user] = arg
84
+ end
85
+
86
+ o.on '--environment=<rails_env>', "The environment you're deploying to" do |arg|
87
+ options[:environment] = arg
88
+ end
89
+ end
90
+ }
91
+ end
92
+
93
+ def notify_of_deploy(options)
94
+ validate_required_options([:revision, :repository, :user, :environment], options)
95
+ Appsignal::Marker.new(
96
+ {
97
+ :revision => options[:revision],
98
+ :repository => options[:repository],
99
+ :user => options[:user]
100
+ },
101
+ ENV['PWD'],
102
+ options[:environment],
103
+ logger
104
+ ).transmit
105
+ end
106
+
107
+ def validate_required_options(required_options, options)
108
+ missing = required_options.select do |required_option|
109
+ options[required_option].blank?
110
+ end
111
+ if missing.any?
112
+ puts "Missing options: #{missing.join(', ')}"
113
+ exit(1)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,30 @@
1
+ module Appsignal
2
+ class Config
3
+ attr_accessor :root_path, :rails_env
4
+
5
+ def initialize(root_path, rails_env, logger=Appsignal.logger)
6
+ @root_path = root_path
7
+ @rails_env = rails_env
8
+ @logger = logger
9
+ end
10
+
11
+ def load
12
+ file = File.join(@root_path, 'config/appsignal.yml')
13
+ unless File.exists?(file)
14
+ @logger.error "config not found at: #{file}"
15
+ return
16
+ end
17
+
18
+ config = YAML.load_file(file)[@rails_env]
19
+ unless config
20
+ @logger.error "config for '#{@rails_env}' not found"
21
+ return
22
+ end
23
+
24
+ config = {:ignore_exceptions => [],
25
+ :endpoint => 'https://push.appsignal.com/1',
26
+ :slow_request_threshold => 200
27
+ }.merge(config.symbolize_keys)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module Appsignal
2
+ class MissingController
3
+ def method_missing(*args, &block)
4
+ end
5
+ end
6
+
7
+ class ExceptionNotification
8
+ attr_reader :env, :exception, :kontroller, :request, :backtrace
9
+
10
+ def initialize(env, exception)
11
+ @exception = exception
12
+ @backtrace = Rails.respond_to?(:backtrace_cleaner) ?
13
+ Rails.backtrace_cleaner.send(:filter, exception.backtrace) :
14
+ exception.backtrace
15
+ end
16
+
17
+ def name
18
+ @exception.class.name
19
+ end
20
+
21
+ def message
22
+ @exception.message
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ module Appsignal
2
+ class Marker
3
+ attr_reader :marker_data, :config, :logger
4
+ ACTION = 'markers'
5
+
6
+ def initialize(marker_data, root_path, rails_env, logger)
7
+ @marker_data = marker_data
8
+ @config = Appsignal::Config.new(root_path, rails_env, logger).load
9
+ @logger = logger
10
+ end
11
+
12
+ def transmit
13
+ begin
14
+ transmitter = Transmitter.new(
15
+ @config[:endpoint], ACTION, @config[:api_key]
16
+ )
17
+ @logger.info "Notifying Appsignal of deploy..."
18
+ result = transmitter.transmit(marker_data)
19
+ if result == '200'
20
+ @logger.info "Appsignal has been notified of this deploy!"
21
+ else
22
+ raise "#{result} at #{transmitter.uri}"
23
+ end
24
+ rescue Exception => e
25
+ message = "Something went wrong while trying to notify Appsignal: #{e}"
26
+ if @logger.respond_to?(:important)
27
+ # This is a Capistrano logger
28
+ @logger.important message
29
+ else
30
+ @logger.error message
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ require 'action_dispatch'
2
+
3
+ module Appsignal
4
+ class Middleware
5
+ def initialize(app, options = {})
6
+ @app, @options = app, options
7
+ end
8
+
9
+ def call(env)
10
+ Appsignal::Transaction.create(env['action_dispatch.request_id'], env)
11
+ @app.call(env)
12
+ rescue Exception => exception
13
+ unless in_ignored_exceptions?(exception)
14
+ Appsignal::Transaction.current.add_exception(
15
+ Appsignal::ExceptionNotification.new(env, exception)
16
+ )
17
+ end
18
+ raise exception
19
+ ensure
20
+ Appsignal::Transaction.current.complete!
21
+ end
22
+
23
+ private
24
+
25
+ def in_ignored_exceptions?(exception)
26
+ Array.wrap(Appsignal.config[:ignore_exceptions]).
27
+ include?(exception.class.name)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ module Appsignal
2
+ class Railtie < Rails::Railtie
3
+ initializer "appsignal.configure_rails_initialization" do |app|
4
+ if Appsignal.active?
5
+ app.middleware.insert_before ActionDispatch::RemoteIp, Appsignal::Middleware
6
+
7
+ Appsignal.subscriber = ActiveSupport::Notifications.subscribe(/^[^!]/) do |*args|
8
+ if Appsignal::Transaction.current
9
+ event = ActiveSupport::Notifications::Event.new(*args)
10
+ if event.name == 'process_action.action_controller'
11
+ Appsignal::Transaction.current.set_process_action_event(event)
12
+ end
13
+ Appsignal::Transaction.current.add_event(event)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,77 @@
1
+ require 'socket'
2
+ require 'appsignal/transaction/transaction_formatter'
3
+
4
+ module Appsignal
5
+ class Transaction
6
+ def self.create(key, env)
7
+ Thread.current[:appsignal_transaction_id] = key
8
+ Appsignal.transactions[key] = Appsignal::Transaction.new(key, env)
9
+ end
10
+
11
+ def self.current
12
+ Appsignal.transactions[Thread.current[:appsignal_transaction_id]]
13
+ end
14
+
15
+ attr_reader :id, :events, :process_action_event, :action, :exception, :env
16
+
17
+ def initialize(id, env)
18
+ @id = id
19
+ @events = []
20
+ @process_action_event = nil
21
+ @exception = nil
22
+ @env = env
23
+ end
24
+
25
+ def request
26
+ ActionDispatch::Request.new(@env)
27
+ end
28
+
29
+ def set_process_action_event(event)
30
+ @process_action_event = event
31
+ if @process_action_event && @process_action_event.payload
32
+ @action = "#{process_action_event.payload[:controller]}#"\
33
+ "#{process_action_event.payload[:action]}"
34
+ end
35
+ end
36
+
37
+ def add_event(event)
38
+ @events << event
39
+ end
40
+
41
+ def add_exception(ex)
42
+ @exception = ex
43
+ end
44
+
45
+ def exception?
46
+ !!exception
47
+ end
48
+
49
+ def slow_request?
50
+ return false unless process_action_event && process_action_event.payload
51
+ Appsignal.config[:slow_request_threshold] <= process_action_event.duration
52
+ end
53
+
54
+ def clear_payload_and_events!
55
+ @process_action_event.payload.clear
56
+ @events.clear
57
+ end
58
+
59
+ def to_hash
60
+ if exception?
61
+ TransactionFormatter.faulty(self)
62
+ elsif slow_request?
63
+ TransactionFormatter.slow(self)
64
+ else
65
+ TransactionFormatter.regular(self)
66
+ end.to_hash
67
+ end
68
+
69
+ def complete!
70
+ Thread.current[:appsignal_transaction_id] = nil
71
+ current_transaction = Appsignal.transactions.delete(@id)
72
+ if process_action_event || exception?
73
+ Appsignal.agent.add_to_queue(current_transaction)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ module Appsignal
2
+ class TransactionFormatter
3
+ class FaultyRequestFormatter < Appsignal::TransactionFormatter
4
+
5
+ def to_hash
6
+ super.merge :exception => formatted_exception
7
+ end
8
+
9
+ protected
10
+
11
+ def_delegators :exception, :backtrace, :name, :message
12
+
13
+ def formatted_exception
14
+ {
15
+ :backtrace => backtrace,
16
+ :exception => name,
17
+ :message => message
18
+ }
19
+ end
20
+
21
+ def basic_process_action_event
22
+ super.merge(
23
+ :environment => filtered_environment,
24
+ :session_data => request.session
25
+ )
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ module Appsignal
2
+ class ParamsSanitizer
3
+ class << self
4
+ def sanitize(params)
5
+ sanitize_hash(params)
6
+ end
7
+
8
+ protected
9
+
10
+ def sanitize_hash(hash)
11
+ out = {}
12
+ hash.each_pair do |key, value|
13
+ out[key] = sanitize_value(value)
14
+ end
15
+ out
16
+ end
17
+
18
+ def sanitize_array(array)
19
+ array.map { |value| sanitize_value(value) }
20
+ end
21
+
22
+ def sanitize_value(value)
23
+ case value
24
+ when Hash
25
+ sanitize_hash(value)
26
+ when Array
27
+ sanitize_array(value)
28
+ when String
29
+ value
30
+ else
31
+ value.inspect
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end