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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +30 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +2 -0
- data/lib/generators/stackify/stackify_generator.rb +13 -0
- data/lib/generators/stackify/templates/stackify.rb +17 -0
- data/lib/stackify-api-ruby.rb +166 -0
- data/lib/stackify/authorization/authorizable.rb +61 -0
- data/lib/stackify/authorization/authorization_client.rb +31 -0
- data/lib/stackify/engine.rb +21 -0
- data/lib/stackify/env_details.rb +108 -0
- data/lib/stackify/error.rb +56 -0
- data/lib/stackify/errors_governor.rb +65 -0
- data/lib/stackify/http_client.rb +50 -0
- data/lib/stackify/logger_client.rb +71 -0
- data/lib/stackify/logger_proxy.rb +35 -0
- data/lib/stackify/logs_sender.rb +78 -0
- data/lib/stackify/metrics/metric.rb +68 -0
- data/lib/stackify/metrics/metric_aggregate.rb +52 -0
- data/lib/stackify/metrics/metrics.rb +88 -0
- data/lib/stackify/metrics/metrics_client.rb +238 -0
- data/lib/stackify/metrics/metrics_queue.rb +26 -0
- data/lib/stackify/metrics/metrics_sender.rb +32 -0
- data/lib/stackify/metrics/monitor.rb +34 -0
- data/lib/stackify/msgs_queue.rb +78 -0
- data/lib/stackify/rack/errors_catcher.rb +17 -0
- data/lib/stackify/schedule_task.rb +23 -0
- data/lib/stackify/scheduler.rb +79 -0
- data/lib/stackify/utils/backtrace.rb +36 -0
- data/lib/stackify/utils/configuration.rb +78 -0
- data/lib/stackify/utils/methods.rb +27 -0
- data/lib/stackify/utils/msg_object.rb +22 -0
- data/lib/stackify/version.rb +3 -0
- data/lib/stackify/workers/add_msg_worker.rb +9 -0
- data/lib/stackify/workers/auth_worker.rb +18 -0
- data/lib/stackify/workers/logs_sender_worker.rb +17 -0
- data/lib/stackify/workers/worker.rb +65 -0
- data/spec/spec_helper.rb +17 -0
- data/stackify-api-ruby.gemspec +25 -0
- 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
|