appsignal 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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