hawkei 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +48 -0
  5. data/.ruby-version +1 -0
  6. data/.tool-versions +1 -0
  7. data/.travis.yml +11 -0
  8. data/Gemfile +12 -0
  9. data/LICENCE +21 -0
  10. data/Makefile +9 -0
  11. data/README.md +17 -0
  12. data/Rakefile +4 -0
  13. data/hawkei.gemspec +28 -0
  14. data/lib/hawkei/api_operation/delete.rb +38 -0
  15. data/lib/hawkei/api_operation/save.rb +57 -0
  16. data/lib/hawkei/api_resource.rb +130 -0
  17. data/lib/hawkei/batch.rb +18 -0
  18. data/lib/hawkei/config.rb +123 -0
  19. data/lib/hawkei/errors.rb +41 -0
  20. data/lib/hawkei/formated_logger.rb +45 -0
  21. data/lib/hawkei/hawkei_object.rb +179 -0
  22. data/lib/hawkei/library_name.rb +3 -0
  23. data/lib/hawkei/message.rb +79 -0
  24. data/lib/hawkei/plugins/rack/middleware.rb +139 -0
  25. data/lib/hawkei/plugins/rails/data.rb +19 -0
  26. data/lib/hawkei/plugins/rails/middleware_data.rb +28 -0
  27. data/lib/hawkei/plugins/rails/railtie.rb +23 -0
  28. data/lib/hawkei/plugins/sidekiq/client_middleware.rb +19 -0
  29. data/lib/hawkei/plugins/sidekiq/load.rb +14 -0
  30. data/lib/hawkei/plugins/sidekiq/server_middleware.rb +48 -0
  31. data/lib/hawkei/plugins.rb +17 -0
  32. data/lib/hawkei/processor/async.rb +50 -0
  33. data/lib/hawkei/processor/batch.rb +84 -0
  34. data/lib/hawkei/processor/worker.rb +113 -0
  35. data/lib/hawkei/request.rb +134 -0
  36. data/lib/hawkei/store.rb +49 -0
  37. data/lib/hawkei/util.rb +180 -0
  38. data/lib/hawkei/version.rb +3 -0
  39. data/lib/hawkei/watcher.rb +15 -0
  40. data/lib/hawkei.rb +170 -0
  41. data/spec/lib/hawkei/api_resource_spec.rb +109 -0
  42. data/spec/lib/hawkei/batch_spec.rb +14 -0
  43. data/spec/lib/hawkei/config_spec.rb +36 -0
  44. data/spec/lib/hawkei/formated_logger_spec.rb +99 -0
  45. data/spec/lib/hawkei/hawkei_object_spec.rb +123 -0
  46. data/spec/lib/hawkei/message_spec.rb +178 -0
  47. data/spec/lib/hawkei/plugins/rack/middleware_spec.rb +88 -0
  48. data/spec/lib/hawkei/plugins/rails/data_spec.rb +22 -0
  49. data/spec/lib/hawkei/plugins/rails/middleware_data_spec.rb +46 -0
  50. data/spec/lib/hawkei/plugins/sidekiq/client_middleware_spec.rb +15 -0
  51. data/spec/lib/hawkei/plugins/sidekiq/server_middleware_spec.rb +58 -0
  52. data/spec/lib/hawkei/processor/async_spec.rb +36 -0
  53. data/spec/lib/hawkei/processor/batch_spec.rb +51 -0
  54. data/spec/lib/hawkei/processor/worker_spec.rb +100 -0
  55. data/spec/lib/hawkei/store_spec.rb +82 -0
  56. data/spec/lib/hawkei/util_spec.rb +132 -0
  57. data/spec/lib/hawkei/watcher_spec.rb +25 -0
  58. data/spec/lib/hawkei_spec.rb +175 -0
  59. data/spec/spec_helper.rb +33 -0
  60. data/spec/support/rack_app.rb +12 -0
  61. metadata +206 -0
@@ -0,0 +1,113 @@
1
+ module Hawkei
2
+ module Processor
3
+ class Worker
4
+
5
+ FLUSH_INTERVAL_SECONDS = 3
6
+
7
+ FLUSH_MESSAGE = Object.new
8
+ SHUTDOWN_MESSAGE = Object.new
9
+
10
+ def initialize(queue, state)
11
+ @queue = queue
12
+ @state = state
13
+
14
+ @batch = Batch.new
15
+ @promises = Concurrent::Array.new
16
+
17
+ @timer = Concurrent::TimerTask.new(execution_interval: FLUSH_INTERVAL_SECONDS) { @queue << FLUSH_MESSAGE }
18
+ @timer.execute
19
+ end
20
+
21
+ def run
22
+ while thread_active?
23
+ message = @queue.pop
24
+
25
+ add_message_to_batch(message)
26
+ end
27
+
28
+ shutdown_worker
29
+ end
30
+
31
+ private
32
+
33
+ ##
34
+ # Add the message to a batch
35
+ #
36
+ # - Add message to a batch if it's not a flush message
37
+ # - flush batch if flush message is receive or batch is full
38
+ def add_message_to_batch(message)
39
+ @batch << message if message != FLUSH_MESSAGE
40
+ flush if message == FLUSH_MESSAGE || @batch.full?
41
+ end
42
+
43
+ ##
44
+ # Shutdown the worker
45
+ #
46
+ # - Close the timer
47
+ # - Retreive the last messages
48
+ # - Wait for all the request to be completed
49
+ #
50
+ # rubocop:disable Lint/HandleExceptions
51
+ def shutdown_worker
52
+ @timer.shutdown
53
+
54
+ begin
55
+ until (message = @queue.pop(true)).nil?
56
+ add_message_to_batch(message)
57
+ end
58
+ rescue ThreadError => _
59
+ end
60
+
61
+ flush
62
+
63
+ @promises.each do |promise|
64
+ promise.wait if promise && !promise.fulfilled?
65
+ end
66
+ end
67
+ # rubocop:enable Lint/HandleExceptions
68
+
69
+ def flush
70
+ return if @batch.empty?
71
+
72
+ send_batch(@batch)
73
+ @batch = Batch.new
74
+ end
75
+
76
+ # rubocop:disable Metrics/AbcSize
77
+ def send_batch(batch)
78
+ promise =
79
+ Concurrent::Promise
80
+ .new { Hawkei::Batch.create(Message.base.merge(data: batch.messages)) }
81
+ .on_success { @promises.delete(promise) }
82
+ .rescue do |error|
83
+ batch.update_retry
84
+
85
+ if error.is_a?(Hawkei::RequestError) && [401, 404].include?(error.http_status)
86
+ return logger.error(error.message)
87
+ end
88
+
89
+ if thread_active? && batch.can_retry?
90
+ Concurrent::ScheduledTask.new(batch.next_retry) { send_batch(batch) }.execute
91
+ else
92
+ @promises.delete(promise)
93
+ end
94
+ end
95
+
96
+ promise.execute
97
+ @promises << promise
98
+ rescue Concurrent::RejectedExecutionError => _
99
+ logger.error('Impossible to start a thread in closing application')
100
+ end
101
+ # rubocop:enable Metrics/AbcSize
102
+
103
+ def thread_active?
104
+ @state.true?
105
+ end
106
+
107
+ def logger
108
+ Hawkei.configurations.logger
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,134 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Request
4
+ #
5
+ # Build request
6
+ class Request
7
+ class << self
8
+
9
+ def execute(params = {})
10
+ klass = new(params)
11
+
12
+ if params[:method] == :post
13
+ klass.post
14
+ elsif params[:method] == :put
15
+ klass.put
16
+ elsif params[:method] == :delete
17
+ klass.delete
18
+ elsif params[:method] == :get
19
+ klass.get
20
+ else
21
+ raise Hawkei::UnknownRequestMethod, "#{params[:method]} haven't been implemented"
22
+ end
23
+ end
24
+
25
+ def post(params = {})
26
+ new(params).post
27
+ end
28
+
29
+ def put(params = {})
30
+ new(params).put
31
+ end
32
+
33
+ def get(params = {})
34
+ new(params).get
35
+ end
36
+
37
+ def delete(params = {})
38
+ new(params).delete
39
+ end
40
+ end
41
+
42
+ attr_reader :params, :http, :request
43
+
44
+ def initialize(params = {})
45
+ @params = params
46
+ @http = Net::HTTP.new(uri.host, uri.port, uri_proxy.host, uri_proxy.port)
47
+
48
+ setup_ssl if params[:use_ssl]
49
+ end
50
+
51
+ def post
52
+ @request = Net::HTTP::Post.new(uri.path)
53
+
54
+ set_header
55
+ request.body = params[:payload]
56
+
57
+ handle_request
58
+ end
59
+
60
+ def put
61
+ @request = Net::HTTP::Put.new(uri.path)
62
+
63
+ set_header
64
+ request.body = params[:payload]
65
+
66
+ handle_request
67
+ end
68
+
69
+ def get
70
+ @request = Net::HTTP::Get.new(uri)
71
+
72
+ set_header
73
+
74
+ handle_request
75
+ end
76
+
77
+ def delete
78
+ @request = Net::HTTP::Delete.new(uri.path)
79
+
80
+ set_header
81
+ request.body = params[:payload]
82
+
83
+ handle_request
84
+ end
85
+
86
+ private
87
+
88
+ def handle_request
89
+ response = http.request(request)
90
+
91
+ case [response.code_type]
92
+ when [Net::HTTPOK], [Net::HTTPCreated], [Net::HTTPAccepted]
93
+ response
94
+ when [Net::HTTPUnauthorized]
95
+ raise RequestError.new(response, message: 'Unauthorized, please check your api key')
96
+ when [Net::HTTPNotFound]
97
+ raise RequestError.new(response, message: 'Requested Resource not found')
98
+ when [Net::HTTPRequestTimeOut]
99
+ raise RequestError.new(response, message: 'Server timeout, verify status of the server')
100
+ when [Net::HTTPInternalServerError]
101
+ raise RequestError.new(response, message: 'Server Error, please check with Hawkei')
102
+ else
103
+ raise RequestError, response
104
+ end
105
+ end
106
+
107
+ def set_header
108
+ params[:headers].each do |(type, value)|
109
+ formated_type = type.to_s.split(/_/).map(&:capitalize).join('-')
110
+ request[formated_type] = value
111
+ end
112
+ end
113
+
114
+ def setup_ssl
115
+ http.use_ssl = true
116
+ http.ca_file = params[:ca_file]
117
+ http.verify_mode = params[:verify_mode]
118
+ end
119
+
120
+ def uri
121
+ @uri ||= URI(params[:url])
122
+ end
123
+
124
+ def uri_proxy
125
+ @uri_proxy ||=
126
+ if params[:proxy]
127
+ URI(params[:proxy])
128
+ else
129
+ OpenStruct.new
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Store
4
+ #
5
+ # Store environment data
6
+ #
7
+ class Store
8
+ class << self
9
+
10
+ def store
11
+ Thread.current[:request_store] ||= {}
12
+ end
13
+
14
+ def clear!
15
+ Thread.current[:request_store] = {}
16
+ end
17
+
18
+ def load_from_hash(object = {})
19
+ clear!
20
+ bulk_set(Util.deep_symbolize_key(object)) if object.is_a?(Hash)
21
+ end
22
+
23
+ def get(key)
24
+ store[key]
25
+ end
26
+ alias [] get
27
+
28
+ def set(key, value)
29
+ store[key] = value
30
+ end
31
+ alias []= set
32
+
33
+ def bulk_set(attributes = {})
34
+ attributes.each do |(key, value)|
35
+ set(key, value)
36
+ end
37
+ end
38
+
39
+ def exist?(key)
40
+ store.key?(key)
41
+ end
42
+
43
+ def delete(key, &block)
44
+ store.delete(key, &block)
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,180 @@
1
+ module Hawkei
2
+ module Util
3
+ class << self
4
+
5
+ DATE_FORMAT = '%Y/%m/%d'.freeze
6
+ OBFUSCATED = '[HIDDEN]'.freeze
7
+ DEFAULT_OBJECT_DEF = %w[id name].freeze
8
+
9
+ ##
10
+ # Remove keys from a Hash
11
+ #
12
+ # @param [Hash] to be excepted
13
+ # @param [List[String]] to be excepted
14
+ #
15
+ # @return [Hash]
16
+ def except_keys(hash, *keys)
17
+ hash.dup.delete_if { |(key, _value)| keys.include?(key) }
18
+ end
19
+
20
+ ##
21
+ # Convert string to camelize
22
+ #
23
+ # @param [String]
24
+ #
25
+ # @example
26
+ # camelize('my_model') => 'MyModel'
27
+ def camelize(string)
28
+ string.split('_').map(&:capitalize).join
29
+ end
30
+
31
+ ##
32
+ # Remove nil value from hash
33
+ #
34
+ # @return [Hash]
35
+ def compact(hash)
36
+ hash.reject { |_, value| value.nil? }
37
+ end
38
+
39
+ ##
40
+ # Remove nil value from hash recursively
41
+ #
42
+ # @return [Hash]
43
+ def deep_compact(object)
44
+ object.each_with_object({}) do |(key, value), result|
45
+ if value.is_a?(Hash)
46
+ value = deep_compact(value)
47
+ result[key] = value if !value.nil? && !value.empty?
48
+ else
49
+ result[key] = value unless value.nil?
50
+ end
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Encodes a hash of parameters in a way that's suitable for use as query
56
+ # parameters in a URI or as form parameters in a request body. This mainly
57
+ # involves escaping special characters from parameter keys and values (e.g.
58
+ # `&`).
59
+ def encode_parameters(params = {})
60
+ params.map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join('&')
61
+ end
62
+
63
+ ##
64
+ # Encodes a string in a way that makes it suitable for use in a set of
65
+ # query parameters in a URI or in a set of form parameters in a request
66
+ # body.
67
+ def url_encode(key)
68
+ CGI.escape(key.to_s).gsub('%5B', '[').gsub('%5D', ']')
69
+ end
70
+
71
+ ##
72
+ # Convert string to underscore
73
+ #
74
+ # @param [String]
75
+ #
76
+ # @example
77
+ # underscore('MyModel') => 'my_model'
78
+ def underscore(string)
79
+ string
80
+ .gsub(/::/, '/')
81
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
82
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
83
+ .tr('-', '_')
84
+ .downcase
85
+ end
86
+
87
+ ##
88
+ # Deep convert hash to underscore case keys
89
+ #
90
+ # @param [Hash] hash to transform
91
+ #
92
+ # @return [Hash] transformed
93
+ def deep_underscore_key(hash_object)
94
+ deep_transform_keys_in_object(hash_object) do |key|
95
+ begin
96
+ underscore(key).to_sym
97
+ rescue StandardError => _e
98
+ key
99
+ end
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Deep convert hash to string keys
105
+ #
106
+ # @param [Hash] hash to transform
107
+ #
108
+ # @return [Hash] transformed
109
+ def deep_stringify_key(hash_object)
110
+ deep_transform_keys_in_object(hash_object) do |key|
111
+ begin
112
+ key.to_s
113
+ rescue StandardError => _e
114
+ key
115
+ end
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Deep convert hash to symbol keys
121
+ #
122
+ # @param [Hash] hash to transform
123
+ #
124
+ # @return [Hash] transformed
125
+ def deep_symbolize_key(hash_object)
126
+ deep_transform_keys_in_object(hash_object) do |key|
127
+ begin
128
+ key.to_sym
129
+ rescue StandardError => _e
130
+ key
131
+ end
132
+ end
133
+ end
134
+
135
+ ##
136
+ # Deep remove key from hash
137
+ #
138
+ # @param [Hash] hash to obfuscate
139
+ #
140
+ # @return [Hash] hash obfuscated
141
+ def deep_obfuscate_value(object, fields, obfuscate_name = OBFUSCATED)
142
+ case object
143
+ when Hash
144
+ object.each_with_object({}) do |(key, value), result|
145
+ result[key] = fields.include?(key.to_s) ? obfuscate_name : deep_obfuscate_value(value, fields)
146
+ end
147
+ when Array
148
+ object.map { |e| deep_obfuscate_value(e, fields) }
149
+ else
150
+ object
151
+ end
152
+ end
153
+
154
+ ##
155
+ # Parse JSON without raise
156
+ #
157
+ def safe_json_parse(data)
158
+ JSON.parse(data.to_s)
159
+ rescue JSON::ParserError
160
+ {}
161
+ end
162
+
163
+ private
164
+
165
+ def deep_transform_keys_in_object(object, &block)
166
+ case object
167
+ when Hash
168
+ object.each_with_object({}) do |(key, value), result|
169
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
170
+ end
171
+ when Array
172
+ object.map { |e| deep_transform_keys_in_object(e, &block) }
173
+ else
174
+ object
175
+ end
176
+ end
177
+
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,3 @@
1
+ module Hawkei # :nodoc:
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,15 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Watcher
4
+ #
5
+ # Create and update groups
6
+ #
7
+ # @example: Usage
8
+ #
9
+ # request = Hawkei::Watcher.create(template_flow: 'My Hawkei', expected_times: 42) #=> #<Hawkei::Response...>
10
+ #
11
+ class Watcher < APIResource
12
+ include Hawkei::APIOperation::Save
13
+ include Hawkei::APIOperation::Delete
14
+ end
15
+ end
data/lib/hawkei.rb ADDED
@@ -0,0 +1,170 @@
1
+ # Lib
2
+ require 'securerandom'
3
+ require 'net/http'
4
+ require 'ostruct'
5
+ require 'json'
6
+ require 'time'
7
+ require 'cgi'
8
+ require 'logger'
9
+ require 'concurrent'
10
+
11
+ # Base
12
+ require 'hawkei/version'
13
+ require 'hawkei/formated_logger'
14
+ require 'hawkei/library_name'
15
+ require 'hawkei/util'
16
+ require 'hawkei/config'
17
+ require 'hawkei/errors'
18
+ require 'hawkei/hawkei_object'
19
+ require 'hawkei/request'
20
+ require 'hawkei/api_resource'
21
+ require 'hawkei/store'
22
+ require 'hawkei/message'
23
+
24
+ # Processor
25
+ require 'hawkei/processor/async'
26
+ require 'hawkei/processor/worker'
27
+ require 'hawkei/processor/batch'
28
+
29
+ # Operations
30
+ require 'hawkei/api_operation/save'
31
+ require 'hawkei/api_operation/delete'
32
+
33
+ # Resources
34
+ require 'hawkei/watcher'
35
+ require 'hawkei/batch'
36
+
37
+ # Plugins
38
+ require 'hawkei/plugins'
39
+
40
+ ##
41
+ # Implementation of the Hawkei
42
+ module Hawkei
43
+ class << self
44
+
45
+ TRACK_EVENTS_CREATE = 'track_events:create'.freeze
46
+ IDENTIFY_EVENTS_CREATE = 'identify_events:create'.freeze
47
+ GROUP_EVENTS_CREATE = 'group_events:create'.freeze
48
+ WATCHERS_CREATE_ACTION = 'watchers:create'.freeze
49
+
50
+ ##
51
+ # @return [Hawkei::Config] configurations
52
+ attr_reader :configurations
53
+
54
+ ##
55
+ # Configures the Hawkei API
56
+ #
57
+ # @example Default configuration
58
+ # Hawkei.configure do |config|
59
+ # config.api_key = 'acc_xx'
60
+ # config.space_name = 'Hawkei'
61
+ # config.environment_name = 'production'
62
+ # end
63
+ def configure
64
+ yield @configurations = Hawkei::Config.new
65
+
66
+ Hawkei.configurations.valid!
67
+ end
68
+
69
+ ##
70
+ # Send a track event to the server
71
+ #
72
+ # @param [String] event name
73
+ # @param [Hash] payload to be send
74
+ # @param [Hash] options for the request
75
+ #
76
+ # @return [Boolean] status of the request
77
+ def track(name, payload = {}, options = {})
78
+ return true unless configurations.enabled
79
+
80
+ payload[:name] = name
81
+ Hawkei::Plugins::Rails::Data.store_data if defined?(Hawkei::Plugins::Rails::Data)
82
+
83
+ payload = Message.extended.merge(payload)
84
+
85
+ processor.enqueue(
86
+ action: TRACK_EVENTS_CREATE,
87
+ payload: payload,
88
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
89
+ )
90
+ end
91
+
92
+ ##
93
+ # Send an identify event to the server
94
+ #
95
+ # @param [String] user id from your database
96
+ # @param [Hash] payload to be send
97
+ # @param [Hash] options for the request
98
+ #
99
+ # @return [Boolean] status of the request
100
+ def identify(user_id, payload = {}, options = {})
101
+ return true unless configurations.enabled
102
+
103
+ payload[:user_id] = user_id
104
+ Hawkei::Plugins::Rails::Data.store_data if defined?(Hawkei::Plugins::Rails::Data)
105
+
106
+ payload = Message.extended.merge(payload)
107
+
108
+ processor.enqueue(
109
+ action: IDENTIFY_EVENTS_CREATE,
110
+ payload: payload,
111
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
112
+ )
113
+ end
114
+
115
+ ##
116
+ # Send a create/update for a group
117
+ #
118
+ # @param [String] group id from your database
119
+ # @param [Hash] payload to be send
120
+ # @param [Hash] options for the request
121
+ #
122
+ # @return [Boolean] status of the request
123
+ def group(group_id, payload = {}, options = {})
124
+ return true unless configurations.enabled
125
+
126
+ payload = Message.base.merge(payload)
127
+ payload[:group_id] = group_id
128
+
129
+ processor.enqueue(
130
+ action: GROUP_EVENTS_CREATE,
131
+ payload: payload,
132
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
133
+ )
134
+ end
135
+
136
+ ##
137
+ # Send a create a watcher
138
+ #
139
+ # @param [String|Integer] template hawkei name or id
140
+ # @param [Hash] payload to be send
141
+ # @param [Hash] options for the request
142
+ #
143
+ # @return [Boolean|Watcher] status of the request
144
+ def watch(flow, payload = {}, options = {})
145
+ return true unless configurations.enabled
146
+
147
+ payload[:template_flow] = flow
148
+ payload = Message.base.merge(payload)
149
+
150
+ payload.delete(:session_tracker_id) if payload.delete(:without_session)
151
+
152
+ if options[:sync]
153
+ Watcher.create(payload, options)
154
+ else
155
+ processor.enqueue(
156
+ action: WATCHERS_CREATE_ACTION,
157
+ payload: payload,
158
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
159
+ )
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def processor
166
+ @processor ||= Processor::Async.new
167
+ end
168
+
169
+ end
170
+ end