injest-client 0.1.2 → 0.1.3
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 +4 -4
- data/lib/README.md +18 -0
- data/lib/injest/configuration.rb +18 -0
- data/lib/injest/http_client.rb +189 -0
- data/lib/injest/middleware.rb +173 -0
- data/lib/injest/worker.rb +6 -0
- data/lib/injest/writer.rb +63 -0
- data/lib/injest.rb +5 -5
- metadata +7 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b596088bca86e19140c5a6896579a1157c93e6f7892ec690697a1cee3e9d731
|
4
|
+
data.tar.gz: bccef6f66027afa03d3ce6d241b0e548a83bd1a2402ba36a1a66b5f8c04099d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f490690c6e154ce55a3d166729c5683a449750824a33aa873dff73f7a152af9c383731ec585c43516fa9884212f53a0ba9a7f42cf72fe0108eb037196b2f0752
|
7
|
+
data.tar.gz: 78ba835c11aff60fcab737943b8e74b8fd51dbd77f58b50c624fa77b91b38a4bf27bbace1d1d2aacda4f2c8a9465324d3a60de737117bb8726121171c8972007
|
data/lib/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Injest Client
|
2
|
+
|
3
|
+
ENV vars
|
4
|
+
|
5
|
+
- `INJEST_STRATEGY`: stdout by default
|
6
|
+
|
7
|
+
Allowed values: Values: stdout | null | http | push
|
8
|
+
|
9
|
+
`http` and `push` have the same behavior.
|
10
|
+
|
11
|
+
- `INJEST_ROOT`: a URL required with strategy `http` or `push`
|
12
|
+
- `INJEST_JWT`: a JWT required with strategy `http` or `push`
|
13
|
+
- `INJEST_CLIENT`: (optional) a string to bind to a specific client
|
14
|
+
|
15
|
+
## TODO:
|
16
|
+
|
17
|
+
- Customization with procs
|
18
|
+
- Tests: https://guides.rubygems.org/make-your-own-gem/#writing-tests
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Injest::Configuration
|
2
|
+
include Singleton
|
3
|
+
|
4
|
+
attr_reader :strategy,
|
5
|
+
:injest_root, :injest_token, :injest_client
|
6
|
+
|
7
|
+
private
|
8
|
+
def initialize
|
9
|
+
@strategy = ENV.fetch('INJEST_STRATEGY', 'stdout')
|
10
|
+
|
11
|
+
if strategy == 'http' || strategy == 'push'
|
12
|
+
@injest_root = ENV.fetch('INJEST_ROOT')
|
13
|
+
@injest_token = ENV.fetch('INJEST_JWT')
|
14
|
+
end
|
15
|
+
|
16
|
+
@injest_client = ENV.fetch('INJEST_CLIENT', nil)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class Injest::HttpClient
|
5
|
+
attr_reader :root_url,
|
6
|
+
:push_url,
|
7
|
+
:search_url,
|
8
|
+
:jwt,
|
9
|
+
:client_code
|
10
|
+
def initialize(configuration)
|
11
|
+
@root_url = configuration.injest_root
|
12
|
+
@client_code = configuration.injest_client
|
13
|
+
if client_code.nil? || client_code == ''
|
14
|
+
@push_url = [root_url, 'api/v1/logs'].join('/')
|
15
|
+
else
|
16
|
+
@push_url = [root_url, 'api/v1/logs', client_code].join('/')
|
17
|
+
end
|
18
|
+
@search_url = [root_url, 'api/v1/logs/search'].join('/')
|
19
|
+
@jwt = configuration.injest_token
|
20
|
+
end
|
21
|
+
|
22
|
+
# Push an audit log
|
23
|
+
def audit(data)
|
24
|
+
push data.merge(log_type: 'audit')
|
25
|
+
end
|
26
|
+
|
27
|
+
def search(page: 1, per: 20, **other)
|
28
|
+
payload = {
|
29
|
+
page: page,
|
30
|
+
per: per,
|
31
|
+
logs_search_form: {}
|
32
|
+
}
|
33
|
+
other.each do |k, v|
|
34
|
+
payload[:logs_search_form][k] = v
|
35
|
+
end
|
36
|
+
|
37
|
+
post_http_request search_url, payload
|
38
|
+
end
|
39
|
+
|
40
|
+
# Push a log
|
41
|
+
def push(data)
|
42
|
+
data = data_as_hash(data)
|
43
|
+
create_log(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
def json(time:, severity:, data:,
|
47
|
+
correlation_id: nil,
|
48
|
+
request_id: nil,
|
49
|
+
partition_key: nil,
|
50
|
+
owner: nil,
|
51
|
+
delta: nil)
|
52
|
+
request_id ||= SecureRandom.uuid
|
53
|
+
data = {
|
54
|
+
request_id: request_id,
|
55
|
+
correlation_id: correlation_id,
|
56
|
+
time: time,
|
57
|
+
human_time: Time.at(time).to_s,
|
58
|
+
log_type: 'json',
|
59
|
+
severity: severity,
|
60
|
+
partition_key: partition_key,
|
61
|
+
owner: owner,
|
62
|
+
delta: delta,
|
63
|
+
data: data_as_hash(data)
|
64
|
+
}
|
65
|
+
create_log data
|
66
|
+
end
|
67
|
+
|
68
|
+
def text(time:, severity:, message:,
|
69
|
+
correlation_id: nil,
|
70
|
+
request_id: nil,
|
71
|
+
partition_key: nil,
|
72
|
+
owner: nil,
|
73
|
+
delta: nil)
|
74
|
+
request_id ||= SecureRandom.uuid
|
75
|
+
data = {
|
76
|
+
request_id: request_id,
|
77
|
+
correlation_id: correlation_id,
|
78
|
+
time: time,
|
79
|
+
human_time: Time.at(time).to_s,
|
80
|
+
log_type: 'text',
|
81
|
+
severity: severity,
|
82
|
+
partition_key: partition_key,
|
83
|
+
owner: owner,
|
84
|
+
delta: delta,
|
85
|
+
text: message
|
86
|
+
}
|
87
|
+
create_log data
|
88
|
+
end
|
89
|
+
|
90
|
+
def process_response!(response)
|
91
|
+
case response.code
|
92
|
+
when '200', '201'
|
93
|
+
unless response.body.empty?
|
94
|
+
JSON.parse(response.body)
|
95
|
+
else
|
96
|
+
{}
|
97
|
+
end
|
98
|
+
else
|
99
|
+
data = JSON.parse(response.body) unless response.body.blank?
|
100
|
+
puts "Endpoint replied with #{response.code} - #{data['error_code']}", data: data
|
101
|
+
raise "Endpoint replied with #{response.code} - #{data['error_code']}", data: data
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def create_log(data)
|
107
|
+
return if ENV['INJEST_STRATEGY'] == 'null'
|
108
|
+
|
109
|
+
if ENV['INJEST_STRATEGY'] == 'stdout'
|
110
|
+
puts "== Injest STDOUT data =="
|
111
|
+
# puts data.inspect
|
112
|
+
print_log(data)
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
post_http_request push_url, data
|
117
|
+
# uri = URI.parse push_url
|
118
|
+
# request = Net::HTTP::Post.new(uri)
|
119
|
+
# request["Content-Type"] = "application/json"
|
120
|
+
# request["Accept"] = "application/json"
|
121
|
+
# request['Authorization'] = "Bearer #{jwt}"
|
122
|
+
# case data
|
123
|
+
# when String
|
124
|
+
# request.body = data
|
125
|
+
# when Hash
|
126
|
+
# request.body = data.to_json
|
127
|
+
# end
|
128
|
+
# req_options = { use_ssl: uri.scheme == "https" }
|
129
|
+
# response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
130
|
+
# http.request(request)
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# process_response!(response)
|
134
|
+
end
|
135
|
+
|
136
|
+
def post_http_request(url, data)
|
137
|
+
uri = URI.parse url
|
138
|
+
request = Net::HTTP::Post.new(uri)
|
139
|
+
request["Content-Type"] = "application/json"
|
140
|
+
request["Accept"] = "application/json"
|
141
|
+
request['Authorization'] = "Bearer #{jwt}"
|
142
|
+
case data
|
143
|
+
when String
|
144
|
+
request.body = data
|
145
|
+
when Hash
|
146
|
+
request.body = data.to_json
|
147
|
+
end
|
148
|
+
req_options = { use_ssl: uri.scheme == "https" }
|
149
|
+
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
150
|
+
http.request(request)
|
151
|
+
end
|
152
|
+
|
153
|
+
process_response!(response)
|
154
|
+
end
|
155
|
+
|
156
|
+
def data_as_hash(data)
|
157
|
+
case data
|
158
|
+
when String
|
159
|
+
JSON.parse(data)
|
160
|
+
when Hash
|
161
|
+
data
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def print_log(data)
|
166
|
+
puts "LOGTYPE '#{data[:log_type]}'"
|
167
|
+
if data[:log_type] != 'audit'
|
168
|
+
puts data.inspect
|
169
|
+
return
|
170
|
+
end
|
171
|
+
|
172
|
+
puts "#{data[:human_time]} - Request #{data[:request_id]}"
|
173
|
+
puts "#{data[:request][:method]} #{data[:request][:path]}"
|
174
|
+
puts ''
|
175
|
+
puts "Subject"
|
176
|
+
puts data[:request][:subject].inspect
|
177
|
+
puts ''
|
178
|
+
puts 'Context'
|
179
|
+
puts data[:request][:context].inspect
|
180
|
+
puts ''
|
181
|
+
puts "Headers"
|
182
|
+
data[:request][:headers].each do |k, v|
|
183
|
+
puts "#{k}: #{v}"
|
184
|
+
end
|
185
|
+
puts ''
|
186
|
+
puts "Response: #{data[:response][:status]}"
|
187
|
+
puts data[:response][:body].inspect
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
class Injest::Middleware
|
2
|
+
HEADERS_BLACKLIST = %w[HTTP_COOKIE].freeze
|
3
|
+
|
4
|
+
attr_reader :configuration
|
5
|
+
attr_reader :request_type, :env, :consumer, :subject
|
6
|
+
|
7
|
+
def initialize(app, processor: nil)
|
8
|
+
@app = app
|
9
|
+
init_configuration
|
10
|
+
|
11
|
+
@processor = processor
|
12
|
+
|
13
|
+
@consumer = nil
|
14
|
+
@subject = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
dup._call(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def _call(env)
|
22
|
+
@env = env
|
23
|
+
set_request_type
|
24
|
+
return @app.call(env) unless trackable?
|
25
|
+
|
26
|
+
@request_started_on = Time.now
|
27
|
+
@status, @headers, @response = @app.call(env)
|
28
|
+
@request_ended_on = Time.now
|
29
|
+
|
30
|
+
set_subject
|
31
|
+
set_consumer
|
32
|
+
|
33
|
+
data = build_data
|
34
|
+
data = @processor.call(env, data) unless @processor.nil?
|
35
|
+
|
36
|
+
append(data)
|
37
|
+
|
38
|
+
[@status, @headers, @response]
|
39
|
+
end
|
40
|
+
|
41
|
+
def init_configuration
|
42
|
+
@configuration = Injest::Configuration.instance
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def append(data)
|
48
|
+
Injest::Writer.instance.append(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_data
|
52
|
+
{
|
53
|
+
request_id: env["action_dispatch.request_id"],
|
54
|
+
time: @request_started_on.to_i,
|
55
|
+
human_time: @request_started_on,
|
56
|
+
type: request_type,
|
57
|
+
request: build_request,
|
58
|
+
response: {
|
59
|
+
status: @status,
|
60
|
+
body: response_body(@response),
|
61
|
+
headers: @headers
|
62
|
+
},
|
63
|
+
delta: @request_ended_on - @request_started_on,
|
64
|
+
log_type: 'audit'
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_request
|
69
|
+
if env.key?('rack.session')
|
70
|
+
auth_scheme = env["rack.session"].fetch('auth_scheme', [])
|
71
|
+
else
|
72
|
+
auth_scheme = []
|
73
|
+
end
|
74
|
+
|
75
|
+
{
|
76
|
+
method: env['REQUEST_METHOD'],
|
77
|
+
server_name: env['SERVER_NAME'],
|
78
|
+
port: env['SERVER_PORT'],
|
79
|
+
query_string: env['QUERY_STRING'],
|
80
|
+
path: env['PATH_INFO'],
|
81
|
+
scheme: env["rack.url_scheme"],
|
82
|
+
remote_addr: env['REMOTE_ADDR'],
|
83
|
+
remote_ip: env['action_dispatch.remote_ip'].to_s,
|
84
|
+
formats: (env["action_dispatch.request.formats"] || []).map(&:to_s),
|
85
|
+
headers: get_request_headers,
|
86
|
+
subject: subject,
|
87
|
+
context: get_context(env['action_dispatch.request.parameters'], env["action_dispatch.request.request_parameters"]),
|
88
|
+
auth_scheme: auth_scheme
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_context(data, params)
|
93
|
+
data ||= {}
|
94
|
+
{
|
95
|
+
controller: data['controller'],
|
96
|
+
action: data['action'],
|
97
|
+
params: params,
|
98
|
+
version: Gem.loaded_specs["injest-client"].version.to_s,
|
99
|
+
consumer: consumer
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_request_headers
|
104
|
+
result = {}
|
105
|
+
env.each do |k, v|
|
106
|
+
if (k.to_s.start_with?('HTTP_') || k.to_s.start_with?('X-')) && !HEADERS_BLACKLIST.include?(k) && !v.blank?
|
107
|
+
result[k] = v
|
108
|
+
end
|
109
|
+
end
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
def response_body(response)
|
114
|
+
if @headers['Content-Type'] == 'text/csv' ||
|
115
|
+
@headers['Content-Type'] == 'application/pdf'
|
116
|
+
return { raw: "* * * RESPONSE NOT SHOWN FOR #{@headers['Content-Type']} * * *" }
|
117
|
+
end
|
118
|
+
|
119
|
+
if request_type == 'api'
|
120
|
+
if response.respond_to?(:body)
|
121
|
+
JSON.parse(response.body) rescue { raw: "* * * Unable to parse JSON response for #{@headers['Content-Type']} * * *" }
|
122
|
+
else
|
123
|
+
response
|
124
|
+
end
|
125
|
+
else
|
126
|
+
if @headers['Content-Type']&.downcase&.include?('application/json')
|
127
|
+
if response.respond_to?(:body)
|
128
|
+
JSON.parse(response.body) rescue { raw: "* * * Unable to parse JSON response for #{@headers['Content-Type']} * * *" }
|
129
|
+
else
|
130
|
+
{ raw: "* * * RESPONSE NOT SHOWN * * *" }
|
131
|
+
end
|
132
|
+
else
|
133
|
+
{ raw: "* * * RESPONSE NOT SHOWN FOR #{@headers['Content-Type']} * * *" }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_consumer
|
139
|
+
if !subject.nil?
|
140
|
+
@consumer = [subject[:type].downcase, subject[:id]].join(':')
|
141
|
+
elsif env.key?("action_dispatch.request.parameters")
|
142
|
+
@consumer = env["action_dispatch.request.parameters"].fetch('_consumer', 'unknown')
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_request_type
|
147
|
+
# TODO: maybe we should check some header instead
|
148
|
+
if env['PATH_INFO'].match?(/.*\.(?:png|css|jpeg|ico|gif)/)
|
149
|
+
@request_type = 'static_asset'
|
150
|
+
elsif env['PATH_INFO'].match?(/^\/api\/v[0-9]+/)
|
151
|
+
@request_type = 'api'
|
152
|
+
elsif env['PATH_INFO'].include?('/sidekiq')
|
153
|
+
@request_type = 'sidekiq'
|
154
|
+
elsif env['PATH_INFO'].include?('/rails')
|
155
|
+
@request_type = 'rails'
|
156
|
+
else
|
157
|
+
@request_type = 'browser'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def set_subject
|
162
|
+
return if env['rack.session'].nil?
|
163
|
+
|
164
|
+
@subject = env["rack.session"].fetch('current_subject', {}).with_indifferent_access
|
165
|
+
if @subject.nil?
|
166
|
+
@subject = env["rack.session"].fetch(:current_subject, {}).with_indifferent_access
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def trackable?
|
171
|
+
!%w[static_asset sidekiq rails].include?(request_type)
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Injest::Writer
|
2
|
+
include Singleton
|
3
|
+
|
4
|
+
attr_reader :configuration, :http_client
|
5
|
+
|
6
|
+
def append(data, sync: false)
|
7
|
+
case configuration.strategy
|
8
|
+
when 'push', 'http'
|
9
|
+
if sync
|
10
|
+
http_client.push(data)
|
11
|
+
else
|
12
|
+
Injest::Worker.perform_async(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
when 'stdout'
|
16
|
+
print(data)
|
17
|
+
|
18
|
+
when 'null'
|
19
|
+
# Do nothing
|
20
|
+
else
|
21
|
+
# Do nothing
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def initialize
|
27
|
+
@configuration = Injest::Configuration.instance
|
28
|
+
@http_client = Injest::HttpClient.new(configuration)
|
29
|
+
end
|
30
|
+
|
31
|
+
def print(data)
|
32
|
+
puts "=== Injest output ==="
|
33
|
+
if data[:log_type] != 'audit'
|
34
|
+
data.each do |k, v|
|
35
|
+
if v.is_a?(Hash)
|
36
|
+
puts " - #{k}:"
|
37
|
+
v.each do |k1, v1|
|
38
|
+
puts " - #{k1}: #{v1}"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
puts " - #{k}: #{v}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "#{data[:request][:method]} #{data[:request][:path]} @ #{data[:human_time]} -- Request #{data[:request_id]}"
|
48
|
+
puts ''
|
49
|
+
puts "Subject"
|
50
|
+
puts " " + data[:request][:subject].inspect
|
51
|
+
puts ''
|
52
|
+
puts 'Context'
|
53
|
+
puts " " + data[:request][:context].inspect
|
54
|
+
puts ''
|
55
|
+
puts "Headers"
|
56
|
+
data[:request][:headers].each do |k, v|
|
57
|
+
puts " - #{k}: #{v}"
|
58
|
+
end
|
59
|
+
puts ''
|
60
|
+
puts "Response: #{data[:response][:status]}"
|
61
|
+
puts data[:response][:body].inspect
|
62
|
+
end
|
63
|
+
end
|
data/lib/injest.rb
CHANGED
@@ -5,9 +5,9 @@ end
|
|
5
5
|
require 'singleton'
|
6
6
|
require 'sidekiq'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
require 'injest/configuration'
|
9
|
+
require 'injest/writer'
|
10
|
+
require 'injest/http_client'
|
11
|
+
require 'injest/worker'
|
12
12
|
|
13
|
-
|
13
|
+
require 'injest/middleware'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: injest-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefano Lampis
|
@@ -58,7 +58,13 @@ executables: []
|
|
58
58
|
extensions: []
|
59
59
|
extra_rdoc_files: []
|
60
60
|
files:
|
61
|
+
- lib/README.md
|
61
62
|
- lib/injest.rb
|
63
|
+
- lib/injest/configuration.rb
|
64
|
+
- lib/injest/http_client.rb
|
65
|
+
- lib/injest/middleware.rb
|
66
|
+
- lib/injest/worker.rb
|
67
|
+
- lib/injest/writer.rb
|
62
68
|
homepage: https://rubygems.org/gems/slampis-injest-client
|
63
69
|
licenses:
|
64
70
|
- MIT
|