closeyourit-ruby 0.3.1 → 0.3.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/closeyourit/background_worker.rb +2 -2
- data/lib/closeyourit/client.rb +24 -0
- data/lib/closeyourit/configuration.rb +15 -4
- data/lib/closeyourit/events/error_event.rb +2 -0
- data/lib/closeyourit/events/log_event.rb +65 -0
- data/lib/closeyourit/log_buffer.rb +63 -0
- data/lib/closeyourit/log_device.rb +39 -0
- data/lib/closeyourit/rails/log_broadcast.rb +30 -0
- data/lib/closeyourit/rails/railtie.rb +10 -0
- data/lib/closeyourit/rails/request_context.rb +17 -3
- data/lib/closeyourit/scope.rb +17 -5
- data/lib/closeyourit/scrubber.rb +7 -3
- data/lib/closeyourit/transport.rb +2 -2
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +71 -3
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82202d393845e74a379b6470a2f5f1c9e093d70840669266fb86624edd326c48
|
|
4
|
+
data.tar.gz: 7a302acfe53c9e0eb866ba407e9133bb63681c3d365126abdbf55368527ab7e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b85ebd77af24ee4aaa41ab447ba434be2ac3892c0ba4c7b09cf5a39069aca91ddb649a179ef621d72c263449452dcb5942c82df5449cd17e37a3494c374b8ed0
|
|
7
|
+
data.tar.gz: 037b9982aad55978b4c124d0751da800983327d18d8786228d8bbd00233f913a0e6ef8dd7e741c06307f11ed425ba2e37b8eae9dc985fba7c2067a9cbafda7fa
|
|
@@ -20,12 +20,12 @@ module CloseYourIt
|
|
|
20
20
|
block.call
|
|
21
21
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
22
22
|
# Mai propagare: la telemetria non deve poter crashare l'app ospite.
|
|
23
|
-
CloseYourIt.
|
|
23
|
+
CloseYourIt.internal_logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
unless accepted
|
|
27
27
|
CloseYourIt.stats.increment(:dropped)
|
|
28
|
-
CloseYourIt.
|
|
28
|
+
CloseYourIt.internal_logger.warn("CloseYourIt background worker: coda piena, evento scartato")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
accepted
|
data/lib/closeyourit/client.rb
CHANGED
|
@@ -4,6 +4,11 @@ module CloseYourIt
|
|
|
4
4
|
# Compone Transport + BackgroundWorker: applica `before_send` e dispatcha
|
|
5
5
|
# l'invio in modo fire-and-forget.
|
|
6
6
|
class Client
|
|
7
|
+
# Tetto di log per singola richiesta a /logs. Il backend rifiuta un batch oltre questo limite
|
|
8
|
+
# (413 R413-LOG-002) scartando l'INTERA richiesta — e il buffer è già stato drenato → log persi.
|
|
9
|
+
# Deve restare ≤ del limite server (LOGS_MAX_BATCH backend = 1000). Vedi #flush_logs.
|
|
10
|
+
LOGS_MAX_BATCH = 1000
|
|
11
|
+
|
|
7
12
|
def initialize(configuration)
|
|
8
13
|
@configuration = configuration
|
|
9
14
|
@transport = Transport.new(configuration)
|
|
@@ -24,6 +29,25 @@ module CloseYourIt
|
|
|
24
29
|
payload
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
# Invia un batch di log come ARRAY a /logs (l'endpoint accetta singolo o array). before_send è
|
|
33
|
+
# applicato a ciascun payload; quelli scartati (nil) non vengono inviati. I payload oltre
|
|
34
|
+
# LOGS_MAX_BATCH sono spezzati in più POST sequenziali (un chunk = un POST), così un flush grande
|
|
35
|
+
# non viene rigettato in blocco dal backend e perso — vedi R3 / LOGS_MAX_BATCH. Un flush entro il
|
|
36
|
+
# limite resta un singolo POST.
|
|
37
|
+
def flush_logs(events)
|
|
38
|
+
return nil if events.nil? || events.empty?
|
|
39
|
+
|
|
40
|
+
payloads = events.map(&:to_h)
|
|
41
|
+
payloads = payloads.filter_map { |payload| @configuration.before_send.call(payload) } if @configuration.before_send
|
|
42
|
+
return nil if payloads.empty?
|
|
43
|
+
|
|
44
|
+
path = events.first.ingest_path(@configuration.project_id)
|
|
45
|
+
payloads.each_slice(LOGS_MAX_BATCH) do |chunk|
|
|
46
|
+
@worker.perform { @transport.send_event(chunk, path: path) }
|
|
47
|
+
end
|
|
48
|
+
payloads
|
|
49
|
+
end
|
|
50
|
+
|
|
27
51
|
def shutdown
|
|
28
52
|
@worker.shutdown
|
|
29
53
|
end
|
|
@@ -22,7 +22,9 @@ module CloseYourIt
|
|
|
22
22
|
:capture_query_bindings, :capture_method_arguments,
|
|
23
23
|
:capture_request, :request_header_allowlist,
|
|
24
24
|
:breadcrumbs_enabled, :max_breadcrumbs, :sample_rate,
|
|
25
|
-
:capture_handled_errors, :report_active_job_errors
|
|
25
|
+
:capture_handled_errors, :report_active_job_errors,
|
|
26
|
+
:logs_enabled, :logs_sample_rate, :logs_batch_size, :logs_flush_interval,
|
|
27
|
+
:capture_rails_logs, :logs_min_level
|
|
26
28
|
attr_writer :release
|
|
27
29
|
attr_reader :excluded_exceptions, :filter_parameters, :scrub_message_patterns
|
|
28
30
|
|
|
@@ -65,6 +67,15 @@ module CloseYourIt
|
|
|
65
67
|
@capture_query_bindings = false
|
|
66
68
|
@capture_method_arguments = false
|
|
67
69
|
|
|
70
|
+
# Log strutturati (CloseYourIt.log / .logger). Master switch + sampling + batching dedicati.
|
|
71
|
+
@logs_enabled = true
|
|
72
|
+
@logs_sample_rate = 1.0
|
|
73
|
+
@logs_batch_size = 50
|
|
74
|
+
@logs_flush_interval = 5
|
|
75
|
+
# Broadcast opt-in di Rails.logger → CloseYourIt.log (default OFF; spedisce solo ≥ soglia).
|
|
76
|
+
@capture_rails_logs = false
|
|
77
|
+
@logs_min_level = :info
|
|
78
|
+
|
|
68
79
|
@filter_parameters = []
|
|
69
80
|
@scrub_message_patterns = []
|
|
70
81
|
end
|
|
@@ -98,9 +109,9 @@ module CloseYourIt
|
|
|
98
109
|
# Logga i warning di configurazione (es. endpoint http://, project_id/endpoint malformati).
|
|
99
110
|
# Non solleva mai: coerente con la filosofia no-op del client. Chiamata da `CloseYourIt.init`.
|
|
100
111
|
def validate!
|
|
101
|
-
CloseYourIt.
|
|
102
|
-
CloseYourIt.
|
|
103
|
-
CloseYourIt.
|
|
112
|
+
CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
|
|
113
|
+
CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
|
|
114
|
+
CloseYourIt.internal_logger.warn(malformed_endpoint_message) if malformed_endpoint?
|
|
104
115
|
self
|
|
105
116
|
end
|
|
106
117
|
|
|
@@ -29,6 +29,8 @@ module CloseYourIt
|
|
|
29
29
|
"timestamp" => @occurred_at,
|
|
30
30
|
"platform" => "ruby",
|
|
31
31
|
"level" => @level,
|
|
32
|
+
# Correlazione log↔errori: stesso trace_id dei log della medesima richiesta (popolato dallo Scope).
|
|
33
|
+
"trace_id" => CloseYourIt::Scope.current.trace_id,
|
|
32
34
|
"environment" => environment,
|
|
33
35
|
"release" => @configuration.release,
|
|
34
36
|
"server_name" => server_name,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
# Voce di log strutturata spedita all'ingest /logs (NON formato Sentry: i log sono uno stream con
|
|
7
|
+
# message/level/attributes/logger). Gli `attributes` passano dallo Scrubber (denylist). `trace_id`
|
|
8
|
+
# è preso dallo Scope corrente (popolato per richiesta) → correlazione log↔errori della stessa request.
|
|
9
|
+
class LogEvent < Event
|
|
10
|
+
# Livelli canonici del backend (enum) + alias dei nomi stile ::Logger. Normalizzati QUI (fonte
|
|
11
|
+
# unica) così ogni costruzione — via CloseYourIt.log, .logger o diretta — produce un livello valido.
|
|
12
|
+
LEVELS = %w[debug info warning error fatal].freeze
|
|
13
|
+
LEVEL_ALIASES = { "warn" => "warning", "err" => "error", "unknown" => "fatal" }.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(message, level:, attributes:, configuration:, logger: nil)
|
|
16
|
+
super(configuration)
|
|
17
|
+
@message = message
|
|
18
|
+
@level = normalize_level(level)
|
|
19
|
+
@attributes = attributes || {}
|
|
20
|
+
@logger = logger
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
compact(
|
|
25
|
+
"event_id" => SecureRandom.uuid.delete("-"),
|
|
26
|
+
"timestamp" => @occurred_at,
|
|
27
|
+
"level" => @level,
|
|
28
|
+
"message" => @message.to_s,
|
|
29
|
+
"attributes" => scrubbed_attributes,
|
|
30
|
+
"logger" => @logger,
|
|
31
|
+
"trace_id" => CloseYourIt::Scope.current.trace_id,
|
|
32
|
+
"environment" => environment,
|
|
33
|
+
"release" => @configuration.release,
|
|
34
|
+
"sdk" => sdk
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ingest_path(project_id)
|
|
39
|
+
"/api/v1/projects/#{project_id}/logs"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def normalize_level(level)
|
|
45
|
+
value = level.to_s.downcase
|
|
46
|
+
value = LEVEL_ALIASES.fetch(value, value)
|
|
47
|
+
LEVELS.include?(value) ? value : "info"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def scrubbed_attributes
|
|
51
|
+
return {} if @attributes.nil? || @attributes.empty?
|
|
52
|
+
|
|
53
|
+
Scrubber.new(@configuration).filter_params(deep_stringify_keys(@attributes))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Chiavi sempre stringa (anche annidate): coerenza col payload JSON e con la denylist dello Scrubber.
|
|
57
|
+
def deep_stringify_keys(value)
|
|
58
|
+
case value
|
|
59
|
+
when Hash then value.each_with_object({}) { |(key, val), acc| acc[key.to_s] = deep_stringify_keys(val) }
|
|
60
|
+
when Array then value.map { |item| deep_stringify_keys(item) }
|
|
61
|
+
else value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
# Buffer in-memory thread-safe dei log: accumula i LogEvent e li flusha in batch verso /logs quando
|
|
7
|
+
# raggiungono `logs_batch_size`, allo scadere di `logs_flush_interval` (timer), o allo shutdown.
|
|
8
|
+
# Riduce le richieste HTTP — i log sono alto-volume, a differenza di errori/metriche uno-a-uno.
|
|
9
|
+
class LogBuffer
|
|
10
|
+
attr_reader :timer # esposto per i test (verifica dell'intervallo configurato)
|
|
11
|
+
|
|
12
|
+
def initialize(client:, configuration:)
|
|
13
|
+
@client = client
|
|
14
|
+
@configuration = configuration
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@events = []
|
|
17
|
+
@timer = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add(event)
|
|
21
|
+
reached_batch = false
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
@events << event
|
|
24
|
+
ensure_timer
|
|
25
|
+
reached_batch = @events.size >= @configuration.logs_batch_size.to_i
|
|
26
|
+
end
|
|
27
|
+
flush if reached_batch
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def flush
|
|
31
|
+
batch = drain
|
|
32
|
+
return if batch.empty?
|
|
33
|
+
|
|
34
|
+
@client.flush_logs(batch)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def shutdown
|
|
38
|
+
@timer&.shutdown
|
|
39
|
+
flush
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def drain
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
events = @events
|
|
47
|
+
@events = []
|
|
48
|
+
events
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Avvia il timer di flush periodico alla prima voce (una sola volta).
|
|
53
|
+
def ensure_timer
|
|
54
|
+
return if @timer
|
|
55
|
+
|
|
56
|
+
interval = @configuration.logs_flush_interval.to_f
|
|
57
|
+
return if interval <= 0
|
|
58
|
+
|
|
59
|
+
@timer = Concurrent::TimerTask.new(execution_interval: interval) { flush }
|
|
60
|
+
@timer.execute
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
# Oggetto Logger-compatibile esposto come `CloseYourIt.logger`: ogni chiamata inoltra a
|
|
5
|
+
# `CloseYourIt.log` (→ ingest /logs). Usabile come logger esplicito dell'app, anche con attributes:
|
|
6
|
+
# CloseYourIt.logger.warn("disco quasi pieno", disk: "sda1")
|
|
7
|
+
# `warn` mappa sul livello `warning` (enum backend); supporta block e `::Logger#add` per drop-in.
|
|
8
|
+
class LogDevice
|
|
9
|
+
# Severità numeriche ::Logger → livelli CloseYourIt (UNKNOWN→fatal).
|
|
10
|
+
SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
|
|
11
|
+
|
|
12
|
+
def debug(message = nil, **attributes, &block) = write("debug", message, attributes, &block)
|
|
13
|
+
def info(message = nil, **attributes, &block) = write("info", message, attributes, &block)
|
|
14
|
+
def warn(message = nil, **attributes, &block) = write("warning", message, attributes, &block)
|
|
15
|
+
def error(message = nil, **attributes, &block) = write("error", message, attributes, &block)
|
|
16
|
+
def fatal(message = nil, **attributes, &block) = write("fatal", message, attributes, &block)
|
|
17
|
+
|
|
18
|
+
def <<(message)
|
|
19
|
+
write("info", message, {})
|
|
20
|
+
message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Compat con ::Logger#add(severity, message = nil, progname = nil).
|
|
24
|
+
def add(severity, message = nil, progname = nil, &block)
|
|
25
|
+
write(SEVERITY_LEVELS.fetch(severity.to_i, "info"), message || progname, {}, &block)
|
|
26
|
+
end
|
|
27
|
+
alias log add
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def write(level, message, attributes, &block)
|
|
32
|
+
# Gate PRIMA del block: `logger.debug { dump_costoso }` non valuta il block se i log sono spenti.
|
|
33
|
+
return unless CloseYourIt.logs_active?
|
|
34
|
+
|
|
35
|
+
message = block.call if block
|
|
36
|
+
CloseYourIt.log(level, message, **attributes)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
module Rails
|
|
7
|
+
# Sink Logger-compatibile agganciato a Rails.logger (broadcast opt-in, `config.capture_rails_logs`):
|
|
8
|
+
# ogni log dell'app ≥ soglia (`logs_min_level`) viene re-inoltrato a CloseYourIt.log → ingest /logs.
|
|
9
|
+
# Sotto soglia: no-op (nessun invio). Non scrive su alcun device (inoltra soltanto).
|
|
10
|
+
class LogBroadcast < ::Logger
|
|
11
|
+
SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
|
|
12
|
+
LEVEL_BY_SYMBOL = { debug: 0, info: 1, warn: 2, warning: 2, error: 3, fatal: 4 }.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(min_level = :info)
|
|
15
|
+
super(nil) # nessun device: inoltra soltanto
|
|
16
|
+
self.level = LEVEL_BY_SYMBOL.fetch(min_level.to_sym, 1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Sovrascrive il punto unico di ::Logger: filtra per soglia e re-inoltra a CloseYourIt.log.
|
|
20
|
+
def add(severity, message = nil, progname = nil)
|
|
21
|
+
severity ||= ::Logger::UNKNOWN
|
|
22
|
+
return true if severity < level
|
|
23
|
+
|
|
24
|
+
text = message || (block_given? ? yield : nil) || progname
|
|
25
|
+
CloseYourIt.log(SEVERITY_LEVELS.fetch(severity, "info"), text) unless text.nil?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "active_job_extension"
|
|
|
6
6
|
require_relative "error_subscriber"
|
|
7
7
|
require_relative "../sidekiq/error_handler"
|
|
8
8
|
require_relative "query_source"
|
|
9
|
+
require_relative "log_broadcast"
|
|
9
10
|
require_relative "../subscribers/slow_query"
|
|
10
11
|
|
|
11
12
|
module CloseYourIt
|
|
@@ -61,6 +62,15 @@ module CloseYourIt
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# Broadcast opt-in di Rails.logger → CloseYourIt.log (config.capture_rails_logs, default OFF).
|
|
66
|
+
# Spedisce solo i log dell'app ≥ logs_min_level. Richiede BroadcastLogger (Rails 7.1+).
|
|
67
|
+
initializer "closeyourit.capture_rails_logs" do
|
|
68
|
+
config = CloseYourIt.configuration
|
|
69
|
+
if config.capture_rails_logs && ::Rails.logger.respond_to?(:broadcast_to)
|
|
70
|
+
::Rails.logger.broadcast_to(CloseYourIt::Rails::LogBroadcast.new(config.logs_min_level))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
64
74
|
# Cattura gli errori dei job Sidekiq (solo se Sidekiq è presente).
|
|
65
75
|
initializer "closeyourit.sidekiq" do
|
|
66
76
|
if defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module CloseYourIt
|
|
4
6
|
module Rails
|
|
5
7
|
# Rack middleware: popola lo Scope con il contesto HTTP della richiesta (method/url/header)
|
|
@@ -15,7 +17,11 @@ module CloseYourIt
|
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def call(env)
|
|
18
|
-
|
|
20
|
+
if enabled?
|
|
21
|
+
# trace_id sempre (correlazione log↔errori), anche con capture_request OFF.
|
|
22
|
+
CloseYourIt::Scope.current.trace_id = trace_id_for(env)
|
|
23
|
+
CloseYourIt::Scope.current.request = build_request(env) if CloseYourIt.configuration.capture_request
|
|
24
|
+
end
|
|
19
25
|
@app.call(env)
|
|
20
26
|
ensure
|
|
21
27
|
CloseYourIt::Scope.reset!
|
|
@@ -23,12 +29,20 @@ module CloseYourIt
|
|
|
23
29
|
|
|
24
30
|
private
|
|
25
31
|
|
|
26
|
-
def
|
|
27
|
-
CloseYourIt.enabled?
|
|
32
|
+
def enabled?
|
|
33
|
+
CloseYourIt.enabled?
|
|
28
34
|
rescue StandardError
|
|
29
35
|
false
|
|
30
36
|
end
|
|
31
37
|
|
|
38
|
+
# Riusa il request id di Rails/Rack se presente (stessa correlazione dei log applicativi),
|
|
39
|
+
# altrimenti ne genera uno.
|
|
40
|
+
def trace_id_for(env)
|
|
41
|
+
env["action_dispatch.request_id"] ||
|
|
42
|
+
env["HTTP_X_REQUEST_ID"].to_s.split(",").first&.strip.then { |id| id&.empty? ? nil : id } ||
|
|
43
|
+
SecureRandom.uuid
|
|
44
|
+
end
|
|
45
|
+
|
|
32
46
|
# Forma `request` Sentry. URL senza query string; header solo dall'allowlist (mai
|
|
33
47
|
# Authorization/Cookie). query_string + IP solo con send_pii (opt-in).
|
|
34
48
|
def build_request(env)
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -34,7 +34,7 @@ module CloseYourIt
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
attr_accessor :request
|
|
37
|
+
attr_accessor :request, :trace_id
|
|
38
38
|
attr_reader :user, :tags, :extra, :contexts, :breadcrumbs
|
|
39
39
|
|
|
40
40
|
def initialize
|
|
@@ -71,17 +71,20 @@ module CloseYourIt
|
|
|
71
71
|
@extra = {}
|
|
72
72
|
@contexts = {}
|
|
73
73
|
@request = nil
|
|
74
|
+
@trace_id = nil
|
|
74
75
|
@breadcrumbs = BreadcrumbBuffer.new(CloseYourIt.configuration.max_breadcrumbs)
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
# Sottoinsieme non vuoto in forma evento Sentry (user/tags/extra/contexts/request),
|
|
78
|
-
# fuso nel payload da ErrorEvent#to_h.
|
|
79
|
+
# fuso nel payload da ErrorEvent#to_h. tags/extra/contexts passano dallo Scrubber (denylist
|
|
80
|
+
# ricorsiva per chiave): il backend NON li ri-scruba (Errors::Ingest::Normalize li conserva
|
|
81
|
+
# verbatim), quindi questa è l'unica rete di sicurezza contro le chiavi sensibili lì — R2.
|
|
79
82
|
def to_event_hash
|
|
80
83
|
{
|
|
81
84
|
"user" => serialize_user,
|
|
82
|
-
"tags" => presence(@tags),
|
|
83
|
-
"extra" => presence(@extra),
|
|
84
|
-
"contexts" => presence(@contexts),
|
|
85
|
+
"tags" => scrub(presence(@tags)),
|
|
86
|
+
"extra" => scrub(presence(@extra)),
|
|
87
|
+
"contexts" => scrub(presence(@contexts)),
|
|
85
88
|
"request" => @request,
|
|
86
89
|
"breadcrumbs" => breadcrumbs_payload
|
|
87
90
|
}.reject { |_key, value| value.nil? }
|
|
@@ -89,6 +92,15 @@ module CloseYourIt
|
|
|
89
92
|
|
|
90
93
|
private
|
|
91
94
|
|
|
95
|
+
# Redige i valori delle chiavi sensibili preservando la struttura (es. contexts.runtime resta
|
|
96
|
+
# intatto, solo i valori sotto chiavi sensibili diventano [FILTERED]). Riusa lo Scrubber della
|
|
97
|
+
# configurazione, lo stesso percorso di breadcrumb.data e degli attributi di log.
|
|
98
|
+
def scrub(hash)
|
|
99
|
+
return hash if hash.nil?
|
|
100
|
+
|
|
101
|
+
Scrubber.new(CloseYourIt.configuration).filter_params(hash)
|
|
102
|
+
end
|
|
103
|
+
|
|
92
104
|
# `user.id` sempre; email/ip_address/username solo con `send_pii` (il backend li strippa
|
|
93
105
|
# comunque — difesa in profondità).
|
|
94
106
|
def serialize_user
|
data/lib/closeyourit/scrubber.rb
CHANGED
|
@@ -6,10 +6,14 @@ module CloseYourIt
|
|
|
6
6
|
class Scrubber
|
|
7
7
|
FILTERED = "[FILTERED]"
|
|
8
8
|
|
|
9
|
-
# Token di chiavi sempre redatti (match per sottostringa, normalizzato).
|
|
9
|
+
# Token di chiavi sempre redatti (match per sottostringa, normalizzato). Allineato 1:1 al regex
|
|
10
|
+
# di backend e client Dart (parità client-side) — vedi Errors/Logs::Ingest::Normalize::SENSITIVE_KEY:
|
|
11
|
+
# /pass|secret|token|api[_-]?key|apikey|authorization|cookie|csrf|credit|card|cvv|ssn|iban/i
|
|
12
|
+
# `pass` copre password/passwd/pass_code/passkey/passphrase; `cookie` copre set-cookie;
|
|
13
|
+
# `credit`+`card` coprono credit_card. La lista letterale precedente ometteva i `pass*` bare → leak.
|
|
10
14
|
DENYLIST = %w[
|
|
11
|
-
|
|
12
|
-
cookie
|
|
15
|
+
pass secret token api_key apikey authorization
|
|
16
|
+
cookie csrf credit card cvv ssn iban
|
|
13
17
|
].freeze
|
|
14
18
|
|
|
15
19
|
STRING_LITERAL = /'(?:[^']|'')*'/
|
|
@@ -24,12 +24,12 @@ module CloseYourIt
|
|
|
24
24
|
CloseYourIt.stats.increment(:sent)
|
|
25
25
|
else
|
|
26
26
|
CloseYourIt.stats.increment(:failed)
|
|
27
|
-
CloseYourIt.
|
|
27
|
+
CloseYourIt.internal_logger.warn("CloseYourIt transport: HTTP #{response.code} su #{path}")
|
|
28
28
|
end
|
|
29
29
|
response
|
|
30
30
|
rescue StandardError => e
|
|
31
31
|
CloseYourIt.stats.increment(:failed)
|
|
32
|
-
CloseYourIt.
|
|
32
|
+
CloseYourIt.internal_logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
|
|
33
33
|
nil
|
|
34
34
|
end
|
|
35
35
|
|
data/lib/closeyourit/version.rb
CHANGED
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -15,12 +15,16 @@ require_relative "closeyourit/events/error_event"
|
|
|
15
15
|
require_relative "closeyourit/events/message_event"
|
|
16
16
|
require_relative "closeyourit/events/slow_query_event"
|
|
17
17
|
require_relative "closeyourit/events/slow_method_event"
|
|
18
|
+
require_relative "closeyourit/events/log_event"
|
|
19
|
+
require_relative "closeyourit/log_device"
|
|
20
|
+
require_relative "closeyourit/log_buffer"
|
|
18
21
|
require_relative "closeyourit/subscribers/slow_query"
|
|
19
22
|
require_relative "closeyourit/instrumenter"
|
|
20
23
|
require_relative "closeyourit/monitor"
|
|
21
24
|
require_relative "closeyourit/client"
|
|
22
25
|
require_relative "closeyourit/rails/capture_exceptions"
|
|
23
26
|
require_relative "closeyourit/rails/request_context"
|
|
27
|
+
require_relative "closeyourit/rails/log_broadcast"
|
|
24
28
|
require_relative "closeyourit/rails/active_job_extension"
|
|
25
29
|
require_relative "closeyourit/rails/error_subscriber"
|
|
26
30
|
require_relative "closeyourit/sidekiq/error_handler"
|
|
@@ -41,8 +45,10 @@ module CloseYourIt
|
|
|
41
45
|
def init
|
|
42
46
|
@configuration = Configuration.new
|
|
43
47
|
@client = nil
|
|
48
|
+
@log_buffer = nil
|
|
44
49
|
yield(@configuration) if block_given?
|
|
45
50
|
@configuration.validate!
|
|
51
|
+
register_shutdown_flush
|
|
46
52
|
@configuration
|
|
47
53
|
end
|
|
48
54
|
|
|
@@ -139,11 +145,46 @@ module CloseYourIt
|
|
|
139
145
|
)
|
|
140
146
|
end
|
|
141
147
|
|
|
148
|
+
# Logger interno della gemma (warning/errori diagnostici su stdout). NON è il logging applicativo:
|
|
149
|
+
# per spedire log strutturati a CloseYourIt usa `CloseYourIt.log` / `CloseYourIt.logger`.
|
|
150
|
+
def internal_logger
|
|
151
|
+
@internal_logger ||= default_internal_logger
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
attr_writer :internal_logger
|
|
155
|
+
|
|
156
|
+
# Logger applicativo Logger-compatibile: inoltra ogni messaggio a `CloseYourIt.log` (→ ingest /logs).
|
|
157
|
+
# CloseYourIt.logger.info("ordine creato", order_id: 1)
|
|
142
158
|
def logger
|
|
143
|
-
@
|
|
159
|
+
@app_logger ||= LogDevice.new
|
|
144
160
|
end
|
|
145
161
|
|
|
146
|
-
|
|
162
|
+
# Costruisce e bufferizza una voce di log strutturata (batch verso /logs, fire-and-forget). Il
|
|
163
|
+
# `level` è normalizzato ai livelli canonici (`:warn`→`warning`, downcase; ignoto→`info`). `logger`
|
|
164
|
+
# opzionale = nome della sorgente del log.
|
|
165
|
+
# CloseYourIt.log(:info, "ordine creato", order_id: 1)
|
|
166
|
+
# CloseYourIt.log(:warn, "retry", logger: "payments", attempt: 3)
|
|
167
|
+
def log(level, message, logger: nil, **attributes)
|
|
168
|
+
return nil unless logs_enabled?
|
|
169
|
+
return nil unless logs_sampled?
|
|
170
|
+
|
|
171
|
+
event = LogEvent.new(message, level: level, attributes: attributes,
|
|
172
|
+
logger: logger, configuration: configuration)
|
|
173
|
+
log_buffer.add(event)
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Vero se i log sono attivi (master switch + flag): usato da LogDevice per NON valutare i block
|
|
178
|
+
# costosi (`logger.debug { dump }`) quando il logging è spento.
|
|
179
|
+
def logs_active?
|
|
180
|
+
logs_enabled?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Forza l'invio dei log bufferizzati (chiamato anche allo shutdown del processo).
|
|
184
|
+
def flush_logs
|
|
185
|
+
@log_buffer&.flush
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
147
188
|
|
|
148
189
|
# Contatori diagnostici del client (accodati/scartati/spediti/falliti).
|
|
149
190
|
# CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: … }
|
|
@@ -157,6 +198,33 @@ module CloseYourIt
|
|
|
157
198
|
@client ||= Client.new(configuration)
|
|
158
199
|
end
|
|
159
200
|
|
|
201
|
+
def log_buffer
|
|
202
|
+
@log_buffer ||= LogBuffer.new(client: client, configuration: configuration)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# I log seguono il master switch del client + il proprio flag dedicato.
|
|
206
|
+
def logs_enabled?
|
|
207
|
+
enabled? && configuration.logs_enabled
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Sampling indipendente dei log (1.0 = tutti, 0.0 = nessuno).
|
|
211
|
+
def logs_sampled?
|
|
212
|
+
rate = configuration.logs_sample_rate.to_f
|
|
213
|
+
return true if rate >= 1.0
|
|
214
|
+
return false if rate <= 0.0
|
|
215
|
+
|
|
216
|
+
Random.rand < rate
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Allo shutdown del processo svuota i log bufferizzati e ferma il timer (una sola registrazione).
|
|
220
|
+
# Senza, i log sotto-batch dei processi brevi (rake/CLI) andrebbero persi all'uscita.
|
|
221
|
+
def register_shutdown_flush
|
|
222
|
+
return if @shutdown_registered
|
|
223
|
+
|
|
224
|
+
@shutdown_registered = true
|
|
225
|
+
at_exit { @log_buffer&.shutdown }
|
|
226
|
+
end
|
|
227
|
+
|
|
160
228
|
def ignored_exception?(exception)
|
|
161
229
|
return true if exception.is_a?(CloseYourIt::Error)
|
|
162
230
|
|
|
@@ -187,7 +255,7 @@ module CloseYourIt
|
|
|
187
255
|
exception.instance_variable_set(CAPTURED_FLAG, true)
|
|
188
256
|
end
|
|
189
257
|
|
|
190
|
-
def
|
|
258
|
+
def default_internal_logger
|
|
191
259
|
::Logger.new($stdout).tap do |l|
|
|
192
260
|
l.level = ::Logger::WARN
|
|
193
261
|
l.progname = "CloseYourIt"
|
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.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -41,14 +41,18 @@ files:
|
|
|
41
41
|
- lib/closeyourit/configuration.rb
|
|
42
42
|
- lib/closeyourit/event.rb
|
|
43
43
|
- lib/closeyourit/events/error_event.rb
|
|
44
|
+
- lib/closeyourit/events/log_event.rb
|
|
44
45
|
- lib/closeyourit/events/message_event.rb
|
|
45
46
|
- lib/closeyourit/events/slow_method_event.rb
|
|
46
47
|
- lib/closeyourit/events/slow_query_event.rb
|
|
47
48
|
- lib/closeyourit/instrumenter.rb
|
|
49
|
+
- lib/closeyourit/log_buffer.rb
|
|
50
|
+
- lib/closeyourit/log_device.rb
|
|
48
51
|
- lib/closeyourit/monitor.rb
|
|
49
52
|
- lib/closeyourit/rails/active_job_extension.rb
|
|
50
53
|
- lib/closeyourit/rails/capture_exceptions.rb
|
|
51
54
|
- lib/closeyourit/rails/error_subscriber.rb
|
|
55
|
+
- lib/closeyourit/rails/log_broadcast.rb
|
|
52
56
|
- lib/closeyourit/rails/query_source.rb
|
|
53
57
|
- lib/closeyourit/rails/railtie.rb
|
|
54
58
|
- lib/closeyourit/rails/request_context.rb
|