stackify-api-ruby 1.0.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +30 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +157 -0
  7. data/Rakefile +2 -0
  8. data/lib/generators/stackify/stackify_generator.rb +13 -0
  9. data/lib/generators/stackify/templates/stackify.rb +17 -0
  10. data/lib/stackify-api-ruby.rb +166 -0
  11. data/lib/stackify/authorization/authorizable.rb +61 -0
  12. data/lib/stackify/authorization/authorization_client.rb +31 -0
  13. data/lib/stackify/engine.rb +21 -0
  14. data/lib/stackify/env_details.rb +108 -0
  15. data/lib/stackify/error.rb +56 -0
  16. data/lib/stackify/errors_governor.rb +65 -0
  17. data/lib/stackify/http_client.rb +50 -0
  18. data/lib/stackify/logger_client.rb +71 -0
  19. data/lib/stackify/logger_proxy.rb +35 -0
  20. data/lib/stackify/logs_sender.rb +78 -0
  21. data/lib/stackify/metrics/metric.rb +68 -0
  22. data/lib/stackify/metrics/metric_aggregate.rb +52 -0
  23. data/lib/stackify/metrics/metrics.rb +88 -0
  24. data/lib/stackify/metrics/metrics_client.rb +238 -0
  25. data/lib/stackify/metrics/metrics_queue.rb +26 -0
  26. data/lib/stackify/metrics/metrics_sender.rb +32 -0
  27. data/lib/stackify/metrics/monitor.rb +34 -0
  28. data/lib/stackify/msgs_queue.rb +78 -0
  29. data/lib/stackify/rack/errors_catcher.rb +17 -0
  30. data/lib/stackify/schedule_task.rb +23 -0
  31. data/lib/stackify/scheduler.rb +79 -0
  32. data/lib/stackify/utils/backtrace.rb +36 -0
  33. data/lib/stackify/utils/configuration.rb +78 -0
  34. data/lib/stackify/utils/methods.rb +27 -0
  35. data/lib/stackify/utils/msg_object.rb +22 -0
  36. data/lib/stackify/version.rb +3 -0
  37. data/lib/stackify/workers/add_msg_worker.rb +9 -0
  38. data/lib/stackify/workers/auth_worker.rb +18 -0
  39. data/lib/stackify/workers/logs_sender_worker.rb +17 -0
  40. data/lib/stackify/workers/worker.rb +65 -0
  41. data/spec/spec_helper.rb +17 -0
  42. data/stackify-api-ruby.gemspec +25 -0
  43. metadata +137 -0
@@ -0,0 +1,31 @@
1
+ module Stackify::Authorizable
2
+
3
+ class AuthorizationClient < Stackify::HttpClient
4
+
5
+ BASE_URI = URI('https://dev.stackify.com/api/Metrics/IdentifyApp')
6
+
7
+ def initialize
8
+ super
9
+ @worker = Stackify::AuthWorker.new
10
+ end
11
+
12
+ def auth attempts, delay_time= 20
13
+ task = auth_task attempts
14
+ @worker.perform delay_time, task
15
+ end
16
+
17
+ def auth_task attempts
18
+ properties = {
19
+ limit: 1,
20
+ attempts: attempts,
21
+ success_condition: lambda do |result|
22
+ result.try(:code) == '200'
23
+ end
24
+ }
25
+ Stackify::ScheduleTask.new properties do
26
+ Stackify.internal_log :debug, 'AthorizationClient: trying to athorize...'
27
+ send_request BASE_URI, Stackify::EnvDetails.instance.auth_info.to_json
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'stackify/rack/errors_catcher'
2
+ module Stackify
3
+ class Engine < ::Rails::Engine
4
+
5
+ if Rails.version > '3.1'
6
+
7
+ initializer 'Stackify set up of logger', group: :all do
8
+ ::Rails.logger = ::Stackify::LoggerProxy.new ::Rails.logger
9
+ Stackify.run
10
+ end
11
+
12
+ initializer 'stackify.middleware', group: :all do |app|
13
+ app.config.app_middleware.use 'Stackify::ErrorsCatcher' do |env|
14
+ Stackify::EnvDetails.instance.request_details = env
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,108 @@
1
+ require 'socket'
2
+ require 'singleton'
3
+ module Stackify
4
+
5
+ class EnvDetails
6
+ include Singleton
7
+ attr_reader :request_details
8
+
9
+ def initialize
10
+ rails_info = defined?(Rails) ? Rails::Info.properties.to_h : nil
11
+ @info = rails_info || { 'Application root' => Dir.pwd, 'Environment' => 'development'}
12
+ @request_details = nil
13
+ @app_name = app_name
14
+ app_location = Stackify.configuration.app_location || @info['Application root']
15
+ @details = {
16
+ 'DeviceName' => Socket.gethostname,
17
+ 'AppLocation' => app_location,
18
+ 'AppName' => @app_name,
19
+ 'ConfiguredAppName' => @app_name,
20
+ 'ConfiguredEnvironmentName' =>@info['Environment']
21
+ }
22
+ end
23
+
24
+ def auth_info
25
+ with_synchronize{ @details }
26
+ end
27
+
28
+ def update_auth_info info
29
+ with_synchronize{ @details.merge! info }
30
+ end
31
+
32
+ def request_details= env
33
+ request = request_instance env
34
+ @request_details = {
35
+ 'webrequest_details' => {
36
+ 'Headers' => headers(env),
37
+ 'Cookies' => cookies(env),
38
+ 'QueryString' => request.try(:GET),
39
+ 'PostData' => request.try(:POST),
40
+ 'PostDataRaw' => request.try(:raw_post),
41
+ 'SessionData' => request.try(:session),
42
+ 'UserIPAddress' => request.try(:remote_ip) || request.try(:ip),
43
+ 'HttpMethod' => request.try(:request_method),
44
+ 'ReferralUrl' => request.referer,
45
+ 'RequestUrl' => request.try(:fullpath),
46
+ 'RequestUrlRoot' => request.try(:base_url),
47
+ 'RequestProtocol' => request.try(:scheme)
48
+ },
49
+ 'server_variables' => server_variables(env),
50
+ 'uuid' => request.uuid
51
+ }
52
+ rescue => e
53
+ warning = 'failed to capture request parameters: %p: %s' % [ e.class, e.message ]
54
+ Stackify.logger.warn warning
55
+ end
56
+
57
+ private
58
+
59
+ def mutex
60
+ @mutex ||= Mutex.new
61
+ end
62
+
63
+ def with_synchronize
64
+ mutex.synchronize{ yield }
65
+ end
66
+
67
+ def app_name
68
+ Stackify.configuration.app_name || Rails.application.config.session_options[:key].sub(/^_/,'').sub(/_session/,'') || 'Unknown'
69
+ end
70
+
71
+ def app_location
72
+ Stackify.configuration.app_location
73
+ end
74
+
75
+ def cookies env
76
+ env['action_dispatch.cookies'].to_h
77
+ end
78
+
79
+ def headers env
80
+ env.reject{ |k| !(k.start_with?'HTTP_') }
81
+ end
82
+
83
+ def server_variables env
84
+ {
85
+ 'server' => server_name(env['SERVER_SOFTWARE']),
86
+ 'version' => server_version(env['SERVER_SOFTWARE'])
87
+ }
88
+ end
89
+
90
+ def server_name str
91
+ str[/(^\S*)\//, 1]
92
+ end
93
+
94
+ def server_version str
95
+ str[/\/(\S*)\s/, 1]
96
+ end
97
+
98
+ def request_instance env
99
+ if defined? ActionDispatch::Request
100
+ ActionDispatch::Request.new env
101
+ else
102
+ Rack::Request.new env
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,56 @@
1
+ module Stackify
2
+
3
+ class StackifiedError < StandardError
4
+
5
+ CONTEXT_PROPERTIES = { 'user' => 'current_user'}
6
+
7
+ attr_reader :context, :exception
8
+
9
+ def initialize(ex, error_binding)
10
+ @exception = ex
11
+ @context = {}
12
+ CONTEXT_PROPERTIES.each do |key , value|
13
+ @context[key] = error_binding.eval(value) if error_binding.local_variable_defined?(value.to_sym)
14
+ end
15
+ end
16
+
17
+ def backtrace
18
+ Stackify::Backtrace.stacktrace @exception.backtrace
19
+ end
20
+
21
+ def source_method
22
+ Stackify::Backtrace.method_name @exception.try{ |e| e.backtrace[0] }
23
+ end
24
+
25
+ def message
26
+ @exception.message
27
+ end
28
+
29
+ def error_type
30
+ @exception.class
31
+ end
32
+
33
+ def to_h
34
+ env = Stackify::EnvDetails.instance
35
+ {
36
+ 'OccurredEpochMillis' => Time.now.to_f*1000,
37
+ 'Error' => {
38
+ 'InnerError' => @exception.try(:cause),
39
+ 'StackTrace' => backtrace,
40
+ 'Message' => message,
41
+ 'ErrorType' => error_type.to_s,
42
+ 'ErrorTypeCode' => nil,
43
+ 'Data' => { user_id: '1'},
44
+ 'SourceMethod' => source_method,
45
+ },
46
+ 'EnvironmentDetail' => env.auth_info,
47
+ 'WebRequestDetail' => env.request_details.try{ |d| d['webrequest_details'] },
48
+ 'ServerVariables' => env.request_details.try{ |d| d['server_variables'] },
49
+ 'CustomerName' => 'Customer',
50
+ 'UserName' => @context['user']
51
+ }
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,65 @@
1
+ module Stackify
2
+ class ErrorsGovernor
3
+ require 'digest'
4
+
5
+ def initialize purge_period=5
6
+ @history = {}
7
+ @@history_lock = Mutex.new
8
+ @purge_period = purge_period
9
+ update_purge_times
10
+ end
11
+
12
+ def can_send? ex
13
+ key = unique_key_of(ex)
14
+ @@history_lock.synchronize do
15
+ epoch_minute = current_minute
16
+ init_history_key_if_not_exists key, epoch_minute
17
+ history_entry = @history[key]
18
+ if history_entry[:epoch_minute] == epoch_minute
19
+ history_entry[:count] += 1
20
+ answer = (history_entry[:count] <= Stackify.configuration.flood_limit) ? true : false
21
+ else
22
+ @history[key]={
23
+ epoch_minute: epoch_minute,
24
+ count: 1
25
+ }
26
+ answer = true
27
+ end
28
+ clear_old_history_entries if time_for_purge_is_come?
29
+ answer
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def unique_key_of ex
36
+ str = "#{ex.backtrace[0]['LineNum']}-#{ex.source_method}-#{ex.error_type}"
37
+ Digest::MD5.hexdigest str
38
+ end
39
+
40
+ def init_history_key_if_not_exists key, minute
41
+ @history[key] ||= {
42
+ epoch_minute: minute,
43
+ count: 0
44
+ }
45
+ end
46
+
47
+ def clear_old_history_entries
48
+ @history.keep_if{ |_key, entry| entry[:epoch_minute] == current_minute }
49
+ update_purge_times
50
+ end
51
+
52
+ def update_purge_times
53
+ @last_purge_minute = current_minute
54
+ @next_purge_minute = @last_purge_minute + @purge_period
55
+ end
56
+
57
+ def current_minute
58
+ Time.now.to_i/60
59
+ end
60
+
61
+ def time_for_purge_is_come?
62
+ !(current_minute < @next_purge_minute)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,50 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ module Stackify
5
+ class HttpClient
6
+
7
+ HEADERS = {
8
+ 'X-Stackify-PV' => 'V1',
9
+ 'X-Stackify-Key' => Stackify.configuration.api_key,
10
+ 'Content-Type' =>'application/json'
11
+ }
12
+ attr_reader :response, :errors
13
+
14
+ def initialize
15
+ end
16
+
17
+ private
18
+
19
+ def send_request uri, body, headers = HEADERS
20
+ @errors = []
21
+ begin
22
+ https = get_https uri
23
+ req = Net::HTTP::Post.new uri.path, initheader = headers
24
+ req.body = body
25
+ Stackify.internal_log :debug, "============Request body=========================="
26
+ Stackify.internal_log :debug, req.body
27
+ Stackify.internal_log :debug, "=================================================="
28
+ @response = https.request req
29
+ rescue => ex
30
+ @errors << ex
31
+ Stackify.log_internal_error('HttpClient: ' + ex.message+ ' Backtrace: '+ Stackify::Backtrace.backtrace_in_line(ex.backtrace))
32
+ false
33
+ end
34
+ end
35
+
36
+ def get_https uri
37
+ if Stackify.configuration.with_proxy
38
+ https = Net::HTTP.new uri.host, uri.port,
39
+ Stackify.configuration.proxy_host,
40
+ Stackify.configuration.proxy_port,
41
+ Stackify.configuration.proxy_user,
42
+ Stackify.configuration.proxy_pass
43
+ else
44
+ https = Net::HTTP.new uri.host, uri.port
45
+ end
46
+ https.use_ssl = true
47
+ https
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,71 @@
1
+ module Stackify
2
+ class LoggerClient
3
+ PERIOD = 1
4
+
5
+ def initialize
6
+ @@errors_governor = Stackify::ErrorsGovernor.new
7
+ end
8
+
9
+ def log level, msg
10
+ if acceptable? level, msg && Stackify.working?
11
+ worker = Stackify::AddMsgWorker.new
12
+ task = log_message_task level, msg
13
+ worker.async_perform PERIOD, task
14
+ end
15
+ end
16
+
17
+ def log_exception level= :error, ex
18
+ if ex.is_a?(Stackify::StackifiedError)
19
+ if acceptable? level, ex.message && Stackify.working?
20
+ if @@errors_governor.can_send? ex
21
+ worker = Stackify::AddMsgWorker.new
22
+ task = log_exception_task level, ex
23
+ worker.async_perform PERIOD, task
24
+ else
25
+ Stackify.internal_log :warn,
26
+ "LoggerClient: logging of exception with message \"#{ex.message}\" is skipped - flood_limit is exceeded"
27
+ end
28
+ end
29
+ else
30
+ Stackify.log_internal_error 'LoggerClient: log_exception should get StackifiedError object'
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def acceptable? level, msg
37
+ Stackify.is_valid? && is_correct_log_level?(level) && is_not_internal_log_message?(msg) && is_appropriate_env?
38
+ end
39
+
40
+ def is_not_internal_log_message? msg
41
+ msg.try(:index, ::Stackify::INTERNAL_LOG_PREFIX).nil?
42
+ end
43
+
44
+ def is_correct_log_level? level
45
+ config_level = Logger.const_get Stackify.configuration.log_level.to_s.upcase
46
+ current_level = Logger.const_get level.to_s.upcase
47
+ current_level >= config_level
48
+ end
49
+
50
+ def is_appropriate_env?
51
+ Stackify.configuration.env.downcase.to_sym == Stackify::EnvDetails.instance.auth_info['ConfiguredEnvironmentName'].downcase.to_sym
52
+ end
53
+
54
+ def log_message_task level, msg
55
+ Stackify::ScheduleTask.new ({limit: 1}) do
56
+ Stackify.msgs_queue.add_msg Stackify::MsgObject.new(level, msg, caller[0]).to_h
57
+ str = "LoggerClient: logging of message #{msg} with level '#{level}' is completed successfully."
58
+ Stackify.internal_log :debug, str
59
+ end
60
+ end
61
+
62
+ def log_exception_task level, ex
63
+ Stackify::ScheduleTask.new ({limit: 1}) do
64
+ Stackify.msgs_queue.add_msg Stackify::MsgObject.new(level, ex.message, caller[0], ex).to_h
65
+ Stackify.internal_log :debug, 'LoggerClient: '+
66
+ 'Logging of the exception %p: %s is completed successfully' % [ ex.class, ex.message ]
67
+ end
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,35 @@
1
+ module Stackify
2
+ class LoggerProxy < Object
3
+
4
+ def initialize logger
5
+ @logger = logger
6
+ @logger.level = Logger.const_get(Stackify.configuration.log_level.to_s.upcase)
7
+ %w( debug info warn error fatal unknown).each do |level|
8
+ LoggerProxy.class_eval do
9
+ define_method level.to_sym do |*args , &block|
10
+ msg = message(args, block)
11
+ Stackify.logger_client.log(level.downcase, msg)
12
+ @logger.send(level.to_sym, args, &block)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ def method_missing(name, *args, &block)
21
+ @logger.send(name, *args, &block)
22
+ end
23
+
24
+ private
25
+
26
+ def message *args, &block
27
+ if block
28
+ block.call
29
+ else
30
+ args[0]
31
+ end
32
+ end
33
+
34
+ end
35
+ end