closeyourit-ruby 0.3.4 → 0.4.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 +4 -4
- data/README.md +3 -3
- data/lib/closeyourit/configuration.rb +10 -1
- data/lib/closeyourit/events/error_event.rb +17 -2
- data/lib/closeyourit/line_cache.rb +41 -0
- data/lib/closeyourit/rails/request_body.rb +87 -0
- data/lib/closeyourit/rails/request_context.rb +5 -1
- data/lib/closeyourit/scope.rb +21 -2
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8d2868773842804dc2c90f163ea4766ea748c59edf8adaa956f257827391e8c
|
|
4
|
+
data.tar.gz: 7fe183c1a6c64226f341b09c7da4cd37bf58bb75409b9afc20c223efc55ffe1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc0267a4f038cd2ab3f6e0af119cacbdb368cdce3f4d07d2a01ea1b8b5f2d2c4f4753fed6d5dcc0b71c5826868c7557eab614d7b0f37f953fd586aff01147c1b
|
|
7
|
+
data.tar.gz: 0b6e57599d21d07d4d2552331aa14d17bed514c7ed0733881d4a7ebd850051ca1271137fac7147f1e2d12c74c69e6e5e265017b256c36abf06ffd6342b458350
|
data/README.md
CHANGED
|
@@ -26,12 +26,12 @@ Caratteristiche:
|
|
|
26
26
|
|
|
27
27
|
## Installazione
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Pubblicata su [RubyGems](https://rubygems.org/gems/closeyourit-ruby):
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
# Gemfile del progetto da monitorare
|
|
33
|
-
gem "closeyourit-ruby"
|
|
34
|
-
# in sviluppo locale:
|
|
33
|
+
gem "closeyourit-ruby"
|
|
34
|
+
# in sviluppo locale, dal checkout:
|
|
35
35
|
# gem "closeyourit-ruby", path: "../closeyourit-ruby"
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -20,7 +20,8 @@ module CloseYourIt
|
|
|
20
20
|
:slow_query_threshold_ms, :slow_method_threshold_ms,
|
|
21
21
|
:send_pii, :obfuscate_sql, :send_server_name,
|
|
22
22
|
:capture_query_bindings, :capture_method_arguments,
|
|
23
|
-
:capture_request, :request_header_allowlist,
|
|
23
|
+
:capture_request, :request_header_allowlist, :context_lines,
|
|
24
|
+
:capture_request_body,
|
|
24
25
|
:breadcrumbs_enabled, :max_breadcrumbs, :sample_rate,
|
|
25
26
|
:capture_handled_errors, :report_active_job_errors,
|
|
26
27
|
:logs_enabled, :logs_sample_rate, :logs_batch_size, :logs_flush_interval,
|
|
@@ -55,6 +56,14 @@ module CloseYourIt
|
|
|
55
56
|
@capture_request = true
|
|
56
57
|
@request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
|
|
57
58
|
|
|
59
|
+
# Righe di sorgente attorno a ogni frame dello stacktrace (pre/context/post). 0 = disattivo.
|
|
60
|
+
@context_lines = 3
|
|
61
|
+
|
|
62
|
+
# Params del body della richiesta nell'evento (`request.data`), estratti LAZY solo quando
|
|
63
|
+
# l'errore accade, sanitizzati e scrubbati (denylist + filter_parameters). Il backend
|
|
64
|
+
# ri-scruba difensivamente. Upload → placeholder, cap 64 KB.
|
|
65
|
+
@capture_request_body = true
|
|
66
|
+
|
|
58
67
|
# Breadcrumbs: cronologia (query offuscate, eventi custom) allegata all'errore.
|
|
59
68
|
@breadcrumbs_enabled = true
|
|
60
69
|
@max_breadcrumbs = 100
|
|
@@ -4,6 +4,7 @@ require "securerandom"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require_relative "../event"
|
|
6
6
|
require_relative "../scrubber"
|
|
7
|
+
require_relative "../line_cache"
|
|
7
8
|
|
|
8
9
|
module CloseYourIt
|
|
9
10
|
# Trasforma un'eccezione Ruby nel **payload evento Sentry** che il backend CloseYourIt ingerisce
|
|
@@ -72,21 +73,35 @@ module CloseYourIt
|
|
|
72
73
|
}
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
# Sentry-style: frame più recente per ultimo; chiavi filename/function/lineno/in_app/abs_path
|
|
76
|
+
# Sentry-style: frame più recente per ultimo; chiavi filename/function/lineno/in_app/abs_path
|
|
77
|
+
# + snippet di sorgente (pre_context/context_line/post_context) quando il file è leggibile.
|
|
76
78
|
def frames(locations)
|
|
77
79
|
return [] if locations.nil?
|
|
78
80
|
|
|
81
|
+
context_lines = @configuration.context_lines.to_i
|
|
79
82
|
locations.reverse.map do |loc|
|
|
80
|
-
{
|
|
83
|
+
frame = {
|
|
81
84
|
"filename" => loc.path,
|
|
82
85
|
"abs_path" => loc.path,
|
|
83
86
|
"function" => loc.label,
|
|
84
87
|
"lineno" => loc.lineno,
|
|
85
88
|
"in_app" => in_app?(loc.path)
|
|
86
89
|
}
|
|
90
|
+
add_context!(frame, loc.path, loc.lineno, context_lines) if context_lines.positive?
|
|
91
|
+
frame
|
|
87
92
|
end
|
|
88
93
|
end
|
|
89
94
|
|
|
95
|
+
def add_context!(frame, path, lineno, count)
|
|
96
|
+
lines = LineCache.lines(path)
|
|
97
|
+
return if lines.nil? || lineno.nil? || lineno < 1 || lineno > lines.size
|
|
98
|
+
|
|
99
|
+
index = lineno - 1
|
|
100
|
+
frame["pre_context"] = lines[[ index - count, 0 ].max...index]
|
|
101
|
+
frame["context_line"] = lines[index]
|
|
102
|
+
frame["post_context"] = lines[index + 1, count] || []
|
|
103
|
+
end
|
|
104
|
+
|
|
90
105
|
def in_app?(path)
|
|
91
106
|
return false if path.nil?
|
|
92
107
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
# Cache in-process delle righe dei file sorgente, per le context lines dei frame
|
|
5
|
+
# (pre_context/context_line/post_context). Bounded: oltre MAX_FILES si svuota per intero —
|
|
6
|
+
# semplice e sufficiente (i file di un'app in errore sono pochi e ricorrenti). Thread-safe.
|
|
7
|
+
module LineCache
|
|
8
|
+
MAX_FILES = 200
|
|
9
|
+
|
|
10
|
+
@cache = {}
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Righe del file (chomp-ate), oppure nil se non leggibile o path sintetico ("(eval)", "(irb)").
|
|
15
|
+
def lines(path)
|
|
16
|
+
return nil if path.nil? || path.empty? || path.start_with?("(")
|
|
17
|
+
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
return @cache[path] if @cache.key?(path)
|
|
20
|
+
|
|
21
|
+
@cache.clear if @cache.size >= MAX_FILES
|
|
22
|
+
@cache[path] = read_lines(path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear
|
|
27
|
+
@mutex.synchronize { @cache.clear }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def read_lines(path)
|
|
33
|
+
return nil unless File.readable?(path)
|
|
34
|
+
|
|
35
|
+
File.readlines(path, chomp: true)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require_relative "../scrubber"
|
|
6
|
+
|
|
7
|
+
module CloseYourIt
|
|
8
|
+
module Rails
|
|
9
|
+
# Estrae i params del body della richiesta (`request.data`) al momento dell'EVENTO — mai
|
|
10
|
+
# eagerly a ogni richiesta (zero overhead sul percorso felice). Preferisce i params già
|
|
11
|
+
# parsati da Rails/Rack presenti in env; ripiega sulla rilettura di rack.input (con rewind)
|
|
12
|
+
# solo per JSON/form ≤ MAX_BODY_BYTES. Output sanitizzato (upload → "[FILE: …]", oggetti →
|
|
13
|
+
# "[OBJECT: …]", stringhe troncate) e scrubbato con la stessa denylist del resto del client.
|
|
14
|
+
module RequestBody
|
|
15
|
+
MAX_BODY_BYTES = 65_536
|
|
16
|
+
MAX_DEPTH = 8
|
|
17
|
+
MAX_STRING = 1024
|
|
18
|
+
|
|
19
|
+
FORM_TYPE = "application/x-www-form-urlencoded"
|
|
20
|
+
JSON_TYPE = "application/json"
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def extract(env)
|
|
24
|
+
params = parsed_params(env) || raw_params(env)
|
|
25
|
+
return nil if params.nil? || params.empty?
|
|
26
|
+
|
|
27
|
+
Scrubber.new(CloseYourIt.configuration).filter_params(sanitize(params, 0))
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Params già parsati a monte (ActionDispatch o Rack): nessuna rilettura del body.
|
|
35
|
+
def parsed_params(env)
|
|
36
|
+
env["action_dispatch.request.request_parameters"] || env["rack.request.form_hash"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def raw_params(env)
|
|
40
|
+
raw = read_body(env)
|
|
41
|
+
return nil if raw.nil? || raw.empty?
|
|
42
|
+
|
|
43
|
+
case env["CONTENT_TYPE"].to_s.split(";").first.to_s.strip.downcase
|
|
44
|
+
when JSON_TYPE
|
|
45
|
+
value = JSON.parse(raw)
|
|
46
|
+
value.is_a?(Hash) ? value : { "_json" => value }
|
|
47
|
+
when FORM_TYPE
|
|
48
|
+
# Fallback flat (stdlib): il percorso reale con nesting passa dai params già parsati.
|
|
49
|
+
URI.decode_www_form(raw).to_h
|
|
50
|
+
end
|
|
51
|
+
rescue JSON::ParserError, ArgumentError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_body(env)
|
|
56
|
+
input = env["rack.input"]
|
|
57
|
+
return nil if input.nil?
|
|
58
|
+
|
|
59
|
+
length = env["CONTENT_LENGTH"].to_i
|
|
60
|
+
return nil if length <= 0 || length > MAX_BODY_BYTES
|
|
61
|
+
|
|
62
|
+
input.rewind if input.respond_to?(:rewind)
|
|
63
|
+
raw = input.read(MAX_BODY_BYTES)
|
|
64
|
+
input.rewind if input.respond_to?(:rewind)
|
|
65
|
+
raw
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sanitize(value, depth)
|
|
69
|
+
return "[TRUNCATED]" if depth > MAX_DEPTH
|
|
70
|
+
|
|
71
|
+
case value
|
|
72
|
+
when Hash then value.each_with_object({}) { |(key, val), acc| acc[key.to_s] = sanitize(val, depth + 1) }
|
|
73
|
+
when Array then value.map { |item| sanitize(item, depth + 1) }
|
|
74
|
+
when String then value.size > MAX_STRING ? "#{value[0, MAX_STRING]}…" : value
|
|
75
|
+
when Numeric, true, false, nil then value
|
|
76
|
+
else
|
|
77
|
+
if value.respond_to?(:original_filename)
|
|
78
|
+
"[FILE: #{value.original_filename}]"
|
|
79
|
+
else
|
|
80
|
+
"[OBJECT: #{value.class.name}]"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -20,7 +20,11 @@ module CloseYourIt
|
|
|
20
20
|
if enabled?
|
|
21
21
|
# trace_id sempre (correlazione log↔errori), anche con capture_request OFF.
|
|
22
22
|
CloseYourIt::Scope.current.trace_id = trace_id_for(env)
|
|
23
|
-
|
|
23
|
+
if CloseYourIt.configuration.capture_request
|
|
24
|
+
CloseYourIt::Scope.current.request = build_request(env)
|
|
25
|
+
# Riferimento all'env per l'estrazione LAZY del body (request.data) a evento costruito.
|
|
26
|
+
CloseYourIt::Scope.current.rack_env = env
|
|
27
|
+
end
|
|
24
28
|
end
|
|
25
29
|
@app.call(env)
|
|
26
30
|
ensure
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -35,7 +35,7 @@ module CloseYourIt
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
attr_accessor :request, :trace_id
|
|
38
|
+
attr_accessor :request, :trace_id, :rack_env
|
|
39
39
|
attr_reader :user, :tags, :extra, :contexts, :breadcrumbs
|
|
40
40
|
|
|
41
41
|
def initialize
|
|
@@ -78,6 +78,7 @@ module CloseYourIt
|
|
|
78
78
|
@extra = {}
|
|
79
79
|
@contexts = {}
|
|
80
80
|
@request = nil
|
|
81
|
+
@rack_env = nil
|
|
81
82
|
@trace_id = nil
|
|
82
83
|
@breadcrumbs = BreadcrumbBuffer.new(CloseYourIt.configuration.max_breadcrumbs)
|
|
83
84
|
@performance_profile = nil
|
|
@@ -93,7 +94,7 @@ module CloseYourIt
|
|
|
93
94
|
"tags" => scrub(presence(@tags)),
|
|
94
95
|
"extra" => scrub(presence(@extra)),
|
|
95
96
|
"contexts" => scrub(presence(@contexts)),
|
|
96
|
-
"request" =>
|
|
97
|
+
"request" => request_payload,
|
|
97
98
|
"breadcrumbs" => breadcrumbs_payload
|
|
98
99
|
}.reject { |_key, value| value.nil? }
|
|
99
100
|
end
|
|
@@ -109,6 +110,24 @@ module CloseYourIt
|
|
|
109
110
|
Scrubber.new(CloseYourIt.configuration).filter_params(hash)
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
# Request context + body params (`request.data`) estratti LAZY qui — cioè solo quando un
|
|
114
|
+
# evento viene davvero costruito, mai sul percorso felice della richiesta.
|
|
115
|
+
def request_payload
|
|
116
|
+
return nil if @request.nil?
|
|
117
|
+
|
|
118
|
+
data = request_body_data
|
|
119
|
+
data ? @request.merge("data" => data) : @request
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def request_body_data
|
|
123
|
+
return nil unless CloseYourIt.configuration.capture_request_body
|
|
124
|
+
return nil if @rack_env.nil?
|
|
125
|
+
|
|
126
|
+
Rails::RequestBody.extract(@rack_env)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
112
131
|
# `user.id` sempre; email/ip_address/username solo con `send_pii` (il backend li strippa
|
|
113
132
|
# comunque — difesa in profondità).
|
|
114
133
|
def serialize_user
|
data/lib/closeyourit/version.rb
CHANGED
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "closeyourit/monitor"
|
|
|
28
28
|
require_relative "closeyourit/client"
|
|
29
29
|
require_relative "closeyourit/rails/capture_exceptions"
|
|
30
30
|
require_relative "closeyourit/rails/request_context"
|
|
31
|
+
require_relative "closeyourit/rails/request_body"
|
|
31
32
|
require_relative "closeyourit/rails/log_broadcast"
|
|
32
33
|
require_relative "closeyourit/rails/net_http_patch"
|
|
33
34
|
require_relative "closeyourit/rails/active_job_extension"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: closeyourit-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/closeyourit/events/slow_method_event.rb
|
|
48
48
|
- lib/closeyourit/events/slow_query_event.rb
|
|
49
49
|
- lib/closeyourit/instrumenter.rb
|
|
50
|
+
- lib/closeyourit/line_cache.rb
|
|
50
51
|
- lib/closeyourit/log_buffer.rb
|
|
51
52
|
- lib/closeyourit/log_device.rb
|
|
52
53
|
- lib/closeyourit/monitor.rb
|
|
@@ -59,6 +60,7 @@ files:
|
|
|
59
60
|
- lib/closeyourit/rails/net_http_patch.rb
|
|
60
61
|
- lib/closeyourit/rails/query_source.rb
|
|
61
62
|
- lib/closeyourit/rails/railtie.rb
|
|
63
|
+
- lib/closeyourit/rails/request_body.rb
|
|
62
64
|
- lib/closeyourit/rails/request_context.rb
|
|
63
65
|
- lib/closeyourit/scope.rb
|
|
64
66
|
- lib/closeyourit/scrubber.rb
|