treblle 1.0.1
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/lib/treblle/data_builder.rb +138 -0
- data/lib/treblle.rb +94 -0
- metadata +44 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ec91ab95442d3471d51d7b7c23baf9c3129e89baa265af101b95db188e1c323b
|
|
4
|
+
data.tar.gz: 44076ace1dc079742312999cfe5e3b73eaee7a2960beb6211dbf6b1e73789445
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bab469e6487bab98fa3e3a92af832e22c58aff4dd8c6ddf02f3d0b55d6c7c51959933426fa84b97f05200f8d2cfc673d3fe868fbb12aad49243914fb9da9c266
|
|
7
|
+
data.tar.gz: d6caa192c3a881d974f993c7e492e63f5c87b6f7966d8ba99b2a737cbb7d2f58c7798c366b6d920bff06d1d2255da9cc602b267d3072744644128a39b6707a48
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
# Gathers and builds body payload to be sent to Treblle in json format.
|
|
6
|
+
# Hides sensitive data based on default values and additional ones provided via env variable.
|
|
7
|
+
class DataBuilder
|
|
8
|
+
DEFAULT_SENSITIVE_FIELDS = Set[
|
|
9
|
+
'card_number',
|
|
10
|
+
'cardNumber',
|
|
11
|
+
'cc',
|
|
12
|
+
'ccv',
|
|
13
|
+
'credit_score',
|
|
14
|
+
'creditScore',
|
|
15
|
+
'password',
|
|
16
|
+
'password_confirmation',
|
|
17
|
+
'passwordConfirmation',
|
|
18
|
+
'pwd',
|
|
19
|
+
'secret',
|
|
20
|
+
'ssn'
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
attr_accessor :env, :status, :started_at, :ended_at, :request, :headers, :json_response, :exception
|
|
24
|
+
|
|
25
|
+
def initialize(params)
|
|
26
|
+
@ended_at = params[:ended_at]
|
|
27
|
+
@env = params[:env]
|
|
28
|
+
@exception = params[:exception]
|
|
29
|
+
@headers = params[:headers]
|
|
30
|
+
@json_response = params[:json_response]
|
|
31
|
+
@request = params[:request]
|
|
32
|
+
@started_at = params[:started_at]
|
|
33
|
+
@status = params[:status]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call
|
|
37
|
+
time_spent = ended_at - started_at
|
|
38
|
+
user_agent = env['HTTP_USER_AGENT']
|
|
39
|
+
ip = env['action_dispatch.remote_ip'].calculate_ip
|
|
40
|
+
request_method = env['REQUEST_METHOD']
|
|
41
|
+
project_id = ENV.fetch('TREBLLE_PROJECT_ID') { '' }
|
|
42
|
+
request_body = request_method.downcase == 'get' ? request.query_parameters : safe_to_json(request.raw_post)
|
|
43
|
+
|
|
44
|
+
data = {
|
|
45
|
+
api_key: ENV.fetch('TREBLLE_API_KEY') { '' },
|
|
46
|
+
project_id: project_id,
|
|
47
|
+
version: Treblle::TREBLLE_VERSION,
|
|
48
|
+
sdk: 'ruby',
|
|
49
|
+
data: {
|
|
50
|
+
server: {
|
|
51
|
+
ip: server_ip,
|
|
52
|
+
timezone: Time.zone.name,
|
|
53
|
+
software: request_headers.try(:[], 'SERVER_SOFTWARE'),
|
|
54
|
+
signature: '',
|
|
55
|
+
protocol: request_headers.try(:[], 'SERVER_PROTOCOL'),
|
|
56
|
+
os: {}
|
|
57
|
+
},
|
|
58
|
+
language: {
|
|
59
|
+
name: 'ruby',
|
|
60
|
+
version: RUBY_VERSION
|
|
61
|
+
},
|
|
62
|
+
request: {
|
|
63
|
+
timestamp: started_at.to_formatted_s(:db),
|
|
64
|
+
ip: ip,
|
|
65
|
+
url: request.original_url,
|
|
66
|
+
user_agent: user_agent,
|
|
67
|
+
method: request_method,
|
|
68
|
+
headers: request_headers,
|
|
69
|
+
body: without_sensitive_attrs(request_body)
|
|
70
|
+
},
|
|
71
|
+
response: {
|
|
72
|
+
headers: headers || {},
|
|
73
|
+
code: status,
|
|
74
|
+
size: json_response&.to_json&.bytesize || 0,
|
|
75
|
+
load_time: time_spent,
|
|
76
|
+
body: without_sensitive_attrs(json_response),
|
|
77
|
+
errors: build_error_object(exception)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
data.to_json
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def without_sensitive_attrs(obj)
|
|
88
|
+
return {} unless obj.present?
|
|
89
|
+
|
|
90
|
+
obj.each do |k, v|
|
|
91
|
+
value = v || k
|
|
92
|
+
if value.is_a?(Hash) || value.is_a?(Array)
|
|
93
|
+
without_sensitive_attrs(value)
|
|
94
|
+
elsif sensitive_attrs.include?(k.to_s)
|
|
95
|
+
obj[k] = '*' * v.to_s.length
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
obj
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Rails.logger.error e.message
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def sensitive_attrs
|
|
104
|
+
@sensitive_attrs ||= user_sensitive_fields.merge(DEFAULT_SENSITIVE_FIELDS)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def user_sensitive_fields
|
|
108
|
+
fields = ENV.fetch('TREBLLE_SENSITIVE_FIELDS') { '' }.gsub(/\s+/, '')
|
|
109
|
+
fields.split(',').to_set
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_error_object(exception)
|
|
113
|
+
return [] unless exception.present?
|
|
114
|
+
|
|
115
|
+
[
|
|
116
|
+
{
|
|
117
|
+
source: 'onError',
|
|
118
|
+
type: exception.class.to_s || 'Unhandled error',
|
|
119
|
+
message: exception.message,
|
|
120
|
+
file: exception.backtrace.try(:first) || ''
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def server_ip
|
|
126
|
+
Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def request_headers
|
|
130
|
+
@request_headers ||= request.headers.env.reject { |key| key.to_s.include?('.') }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def safe_to_json(obj)
|
|
134
|
+
JSON.parse(obj)
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
{}
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/treblle.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'treblle/data_builder'
|
|
6
|
+
|
|
7
|
+
# Treblle middleware for request interception and gathering.
|
|
8
|
+
class Treblle
|
|
9
|
+
TREBLLE_URI = 'https://rocknrolla.treblle.com'
|
|
10
|
+
TREBLLE_VERSION = '0.6'
|
|
11
|
+
|
|
12
|
+
def initialize(app)
|
|
13
|
+
@app = app
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
request = ActionDispatch::Request.new(env)
|
|
18
|
+
started_at = Time.now
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
status, headers, response = @app.call(env)
|
|
22
|
+
to_parse = response.clone
|
|
23
|
+
to_parse = to_parse.try(:body) if to_parse.respond_to?(:body)
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
json_response = JSON.parse(to_parse)
|
|
27
|
+
rescue JSON::ParserError, TypeError
|
|
28
|
+
# continue with original values when unable to parse reponse to json
|
|
29
|
+
return [status, headers, response]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
params = {
|
|
33
|
+
ended_at: Time.now,
|
|
34
|
+
env: env,
|
|
35
|
+
headers: headers,
|
|
36
|
+
json_response: json_response,
|
|
37
|
+
request: request,
|
|
38
|
+
started_at: started_at,
|
|
39
|
+
status: status
|
|
40
|
+
}
|
|
41
|
+
capture(params)
|
|
42
|
+
rescue Exception => e
|
|
43
|
+
status = status_code_for_exception(e)
|
|
44
|
+
params = {
|
|
45
|
+
ended_at: Time.now,
|
|
46
|
+
env: env,
|
|
47
|
+
exception: e,
|
|
48
|
+
headers: headers,
|
|
49
|
+
request: request,
|
|
50
|
+
started_at: started_at,
|
|
51
|
+
status: status
|
|
52
|
+
}
|
|
53
|
+
# send error payload to treblle, but raise exception as well
|
|
54
|
+
capture(params)
|
|
55
|
+
raise e
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[status, headers, response]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Creates data to send in a format that Treblle expects and does the sending in a new thread
|
|
62
|
+
# for perfomance reasons.
|
|
63
|
+
# @param [Object] params
|
|
64
|
+
def capture(params)
|
|
65
|
+
data = DataBuilder.new(params).call
|
|
66
|
+
return if data&.bytesize.to_i > 2.megabytes # ignore the capture for unnaturally large requests
|
|
67
|
+
|
|
68
|
+
Thread.new do
|
|
69
|
+
send_to_treblle(data)
|
|
70
|
+
end
|
|
71
|
+
rescue Exception => e
|
|
72
|
+
Rails.logger.error(e.message)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def send_to_treblle(data)
|
|
76
|
+
uri = URI(TREBLLE_URI)
|
|
77
|
+
req = Net::HTTP::Post.new(uri)
|
|
78
|
+
req['Content-Type'] = 'application/json'
|
|
79
|
+
req['x-api-key'] = ENV.fetch('TREBLLE_API_KEY') { '' }
|
|
80
|
+
req.body = data
|
|
81
|
+
puts data
|
|
82
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
83
|
+
http.request(req)
|
|
84
|
+
end
|
|
85
|
+
res.try(:body)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def status_code_for_exception(exception)
|
|
89
|
+
exception_wrapper = ActionDispatch::ExceptionWrapper.new(nil, exception)
|
|
90
|
+
exception_wrapper.status_code
|
|
91
|
+
rescue
|
|
92
|
+
500
|
|
93
|
+
end
|
|
94
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: treblle
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hrvoje Grdic
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-09-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: TreblleSDK for ruby
|
|
14
|
+
email: hrvoje.grdic@bitlab.hr
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/treblle.rb
|
|
20
|
+
- lib/treblle/data_builder.rb
|
|
21
|
+
homepage: https://rubygems.org/gems/treblle
|
|
22
|
+
licenses:
|
|
23
|
+
- MIT
|
|
24
|
+
metadata: {}
|
|
25
|
+
post_install_message:
|
|
26
|
+
rdoc_options: []
|
|
27
|
+
require_paths:
|
|
28
|
+
- lib
|
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
requirements: []
|
|
40
|
+
rubygems_version: 3.1.6
|
|
41
|
+
signing_key:
|
|
42
|
+
specification_version: 4
|
|
43
|
+
summary: TreblleSDK
|
|
44
|
+
test_files: []
|