closeyourit-ruby 0.2.1 → 0.3.2
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 +17 -0
- data/lib/closeyourit/background_worker.rb +11 -2
- data/lib/closeyourit/client.rb +16 -1
- data/lib/closeyourit/configuration.rb +41 -3
- 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 +2 -1
- data/lib/closeyourit/stats.rb +37 -0
- data/lib/closeyourit/transport.rb +12 -4
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +78 -3
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e319cd48ab53d0933f14344dc8ea2ac503ed7537b8111789fd540cf6bc3d65f
|
|
4
|
+
data.tar.gz: 2f7d6223045d8c9aea2ad4d1b9b6bac4cd1420cbc8abe2bd91622855cb45dfae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d37a064316618dcbee1b2a82626f950dc62e20dc22db3f7b8c6d14373ae1d307a20570ead272959536a1a337cceb3a13375e87edb5c2af23c1eeceae14fbee1
|
|
7
|
+
data.tar.gz: 937873c344d55dd765cedae07d1b7d05fb531012c22b616f4e22d6a8130fd26059a65cc8e949a1336b3ab97788d8e930118e451a13e1a51c2e4c6bccdcdb0efb
|
data/README.md
CHANGED
|
@@ -171,6 +171,23 @@ Privacy-by-default (`send_pii = false`). In sintesi:
|
|
|
171
171
|
> Rischio residuo: `exception.value` e i nomi tabella/colonna nello SQL possono contenere dati di
|
|
172
172
|
> dominio. Usa `before_send`/`scrub_message_patterns` per azzerarli. Dettaglio in [`PDR.md` §9](PDR.md).
|
|
173
173
|
|
|
174
|
+
## Diagnostica
|
|
175
|
+
|
|
176
|
+
Il trasporto è fire-and-forget: non solleva mai e non blocca la request. Per non lasciare
|
|
177
|
+
fallimenti silenziosi, ogni risposta HTTP non-2xx (es. `401` token errato, `404` progetto
|
|
178
|
+
inesistente) viene loggata a `warn`, così come gli eventi scartati a coda piena. I contatori
|
|
179
|
+
sono ispezionabili a runtime:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
CloseYourIt.stats.to_h
|
|
183
|
+
# => { enqueued: 128, dropped: 0, sent: 126, failed: 2 }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- `enqueued` — eventi accettati per l'invio
|
|
187
|
+
- `dropped` — scartati perché la coda async era piena (mai backpressure)
|
|
188
|
+
- `sent` — risposta HTTP 2xx
|
|
189
|
+
- `failed` — errore di rete o status non-2xx (vedi i log a `warn`)
|
|
190
|
+
|
|
174
191
|
## Sviluppo
|
|
175
192
|
|
|
176
193
|
```bash
|
|
@@ -13,13 +13,22 @@ module CloseYourIt
|
|
|
13
13
|
@executor = build_executor(threads.to_i, max_queue)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Ritorna true se l'evento è stato accettato (o eseguito sincrono), false se scartato
|
|
17
|
+
# perché la coda era piena (`fallback_policy: :discard`). Mai backpressure sulla request.
|
|
16
18
|
def perform(&block)
|
|
17
|
-
@executor.post do
|
|
19
|
+
accepted = @executor.post do
|
|
18
20
|
block.call
|
|
19
21
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
20
22
|
# Mai propagare: la telemetria non deve poter crashare l'app ospite.
|
|
21
|
-
CloseYourIt.
|
|
23
|
+
CloseYourIt.internal_logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
unless accepted
|
|
27
|
+
CloseYourIt.stats.increment(:dropped)
|
|
28
|
+
CloseYourIt.internal_logger.warn("CloseYourIt background worker: coda piena, evento scartato")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
accepted
|
|
23
32
|
end
|
|
24
33
|
|
|
25
34
|
def shutdown(timeout = 1)
|
data/lib/closeyourit/client.rb
CHANGED
|
@@ -19,10 +19,25 @@ module CloseYourIt
|
|
|
19
19
|
return nil if payload.nil?
|
|
20
20
|
|
|
21
21
|
path = event.ingest_path(@configuration.project_id)
|
|
22
|
-
@worker.perform { @transport.send_event(payload, path: path) }
|
|
22
|
+
accepted = @worker.perform { @transport.send_event(payload, path: path) }
|
|
23
|
+
CloseYourIt.stats.increment(:enqueued) if accepted
|
|
23
24
|
payload
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# Invia un batch di log come ARRAY a /logs (l'endpoint accetta singolo o array). before_send è
|
|
28
|
+
# applicato a ciascun payload; quelli scartati (nil) non vengono inviati.
|
|
29
|
+
def flush_logs(events)
|
|
30
|
+
return nil if events.nil? || events.empty?
|
|
31
|
+
|
|
32
|
+
payloads = events.map(&:to_h)
|
|
33
|
+
payloads = payloads.filter_map { |payload| @configuration.before_send.call(payload) } if @configuration.before_send
|
|
34
|
+
return nil if payloads.empty?
|
|
35
|
+
|
|
36
|
+
path = events.first.ingest_path(@configuration.project_id)
|
|
37
|
+
@worker.perform { @transport.send_event(payloads, path: path) }
|
|
38
|
+
payloads
|
|
39
|
+
end
|
|
40
|
+
|
|
26
41
|
def shutdown
|
|
27
42
|
@worker.shutdown
|
|
28
43
|
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
|
|
@@ -95,9 +106,12 @@ module CloseYourIt
|
|
|
95
106
|
true
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
# Logga i warning di configurazione (es. endpoint http
|
|
109
|
+
# Logga i warning di configurazione (es. endpoint http://, project_id/endpoint malformati).
|
|
110
|
+
# Non solleva mai: coerente con la filosofia no-op del client. Chiamata da `CloseYourIt.init`.
|
|
99
111
|
def validate!
|
|
100
|
-
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?
|
|
101
115
|
self
|
|
102
116
|
end
|
|
103
117
|
|
|
@@ -133,11 +147,35 @@ module CloseYourIt
|
|
|
133
147
|
nil
|
|
134
148
|
end
|
|
135
149
|
|
|
150
|
+
UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
151
|
+
|
|
136
152
|
def insecure_endpoint?
|
|
137
153
|
uri = parsed_endpoint
|
|
138
154
|
!uri.nil? && uri.scheme != "https"
|
|
139
155
|
end
|
|
140
156
|
|
|
157
|
+
# Avvisa se il project_id è valorizzato ma non sembra uno UUID (l'errore tipico è incollare
|
|
158
|
+
# uno slug/nome al posto dell'id). Non blocca: il server è l'autorità sulla validità.
|
|
159
|
+
def malformed_project_id?
|
|
160
|
+
!blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def malformed_project_id_message
|
|
164
|
+
"CloseYourIt: project_id (#{project_id}) non ha forma UUID — verifica di aver copiato l'id corretto."
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Avvisa se endpoint_url è valorizzato ma non parsabile o privo di host.
|
|
168
|
+
def malformed_endpoint?
|
|
169
|
+
return false if blank?(endpoint_url)
|
|
170
|
+
|
|
171
|
+
uri = parsed_endpoint
|
|
172
|
+
uri.nil? || blank?(uri.host)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def malformed_endpoint_message
|
|
176
|
+
"CloseYourIt: endpoint_url (#{endpoint_url}) non è un URL valido (host mancante)."
|
|
177
|
+
end
|
|
178
|
+
|
|
141
179
|
def insecure_endpoint_message
|
|
142
180
|
tail = production? ? "Telemetria DISABILITATA in production." : "Consentito solo in sviluppo."
|
|
143
181
|
"CloseYourIt: endpoint_url usa http:// non sicuro (#{endpoint_url}) — il token viaggerebbe in chiaro. #{tail}"
|
|
@@ -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,6 +71,7 @@ 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
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module CloseYourIt
|
|
6
|
+
# Contatori diagnostici thread-safe del client: quanti eventi sono stati accodati,
|
|
7
|
+
# scartati (coda piena), spediti con successo o falliti (rete o status non-2xx).
|
|
8
|
+
# Servono a rendere visibili i fallimenti silenziosi del trasporto fire-and-forget.
|
|
9
|
+
#
|
|
10
|
+
# CloseYourIt.stats.to_h # => { enqueued: 12, dropped: 0, sent: 11, failed: 1 }
|
|
11
|
+
class Stats
|
|
12
|
+
COUNTERS = %i[enqueued dropped sent failed].freeze
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@counters = COUNTERS.to_h { |name| [ name, Concurrent::AtomicFixnum.new(0) ] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def increment(name)
|
|
19
|
+
counter = @counters.fetch(name)
|
|
20
|
+
counter.increment
|
|
21
|
+
counter.value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](name)
|
|
25
|
+
@counters.fetch(name).value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
@counters.transform_values(&:value)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset!
|
|
33
|
+
@counters.each_value { |counter| counter.value = 0 }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -10,8 +10,8 @@ module CloseYourIt
|
|
|
10
10
|
class Transport
|
|
11
11
|
OPEN_TIMEOUT = 2
|
|
12
12
|
READ_TIMEOUT = 3
|
|
13
|
-
# Net::HTTP non segue i redirect
|
|
14
|
-
#
|
|
13
|
+
# Net::HTTP non segue i redirect: l'host canonico può rispondere 301 (es. apex → www).
|
|
14
|
+
# Ri-POSTiamo a Location preservando metodo + body, così l'evento non si perde in silenzio.
|
|
15
15
|
MAX_REDIRECTS = 2
|
|
16
16
|
|
|
17
17
|
def initialize(configuration)
|
|
@@ -19,9 +19,17 @@ module CloseYourIt
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def send_event(payload, path:)
|
|
22
|
-
post(payload, path)
|
|
22
|
+
response = post(payload, path)
|
|
23
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
24
|
+
CloseYourIt.stats.increment(:sent)
|
|
25
|
+
else
|
|
26
|
+
CloseYourIt.stats.increment(:failed)
|
|
27
|
+
CloseYourIt.internal_logger.warn("CloseYourIt transport: HTTP #{response.code} su #{path}")
|
|
28
|
+
end
|
|
29
|
+
response
|
|
23
30
|
rescue StandardError => e
|
|
24
|
-
CloseYourIt.
|
|
31
|
+
CloseYourIt.stats.increment(:failed)
|
|
32
|
+
CloseYourIt.internal_logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
|
|
25
33
|
nil
|
|
26
34
|
end
|
|
27
35
|
|
data/lib/closeyourit/version.rb
CHANGED
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "closeyourit/configuration"
|
|
|
7
7
|
require_relative "closeyourit/breadcrumb"
|
|
8
8
|
require_relative "closeyourit/scope"
|
|
9
9
|
require_relative "closeyourit/scrubber"
|
|
10
|
+
require_relative "closeyourit/stats"
|
|
10
11
|
require_relative "closeyourit/background_worker"
|
|
11
12
|
require_relative "closeyourit/transport"
|
|
12
13
|
require_relative "closeyourit/event"
|
|
@@ -14,12 +15,16 @@ require_relative "closeyourit/events/error_event"
|
|
|
14
15
|
require_relative "closeyourit/events/message_event"
|
|
15
16
|
require_relative "closeyourit/events/slow_query_event"
|
|
16
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"
|
|
17
21
|
require_relative "closeyourit/subscribers/slow_query"
|
|
18
22
|
require_relative "closeyourit/instrumenter"
|
|
19
23
|
require_relative "closeyourit/monitor"
|
|
20
24
|
require_relative "closeyourit/client"
|
|
21
25
|
require_relative "closeyourit/rails/capture_exceptions"
|
|
22
26
|
require_relative "closeyourit/rails/request_context"
|
|
27
|
+
require_relative "closeyourit/rails/log_broadcast"
|
|
23
28
|
require_relative "closeyourit/rails/active_job_extension"
|
|
24
29
|
require_relative "closeyourit/rails/error_subscriber"
|
|
25
30
|
require_relative "closeyourit/sidekiq/error_handler"
|
|
@@ -40,8 +45,10 @@ module CloseYourIt
|
|
|
40
45
|
def init
|
|
41
46
|
@configuration = Configuration.new
|
|
42
47
|
@client = nil
|
|
48
|
+
@log_buffer = nil
|
|
43
49
|
yield(@configuration) if block_given?
|
|
44
50
|
@configuration.validate!
|
|
51
|
+
register_shutdown_flush
|
|
45
52
|
@configuration
|
|
46
53
|
end
|
|
47
54
|
|
|
@@ -138,11 +145,52 @@ module CloseYourIt
|
|
|
138
145
|
)
|
|
139
146
|
end
|
|
140
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)
|
|
141
158
|
def logger
|
|
142
|
-
@
|
|
159
|
+
@app_logger ||= LogDevice.new
|
|
160
|
+
end
|
|
161
|
+
|
|
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
|
|
143
187
|
end
|
|
144
188
|
|
|
145
|
-
|
|
189
|
+
# Contatori diagnostici del client (accodati/scartati/spediti/falliti).
|
|
190
|
+
# CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: … }
|
|
191
|
+
def stats
|
|
192
|
+
@stats ||= Stats.new
|
|
193
|
+
end
|
|
146
194
|
|
|
147
195
|
private
|
|
148
196
|
|
|
@@ -150,6 +198,33 @@ module CloseYourIt
|
|
|
150
198
|
@client ||= Client.new(configuration)
|
|
151
199
|
end
|
|
152
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
|
+
|
|
153
228
|
def ignored_exception?(exception)
|
|
154
229
|
return true if exception.is_a?(CloseYourIt::Error)
|
|
155
230
|
|
|
@@ -180,7 +255,7 @@ module CloseYourIt
|
|
|
180
255
|
exception.instance_variable_set(CAPTURED_FLAG, true)
|
|
181
256
|
end
|
|
182
257
|
|
|
183
|
-
def
|
|
258
|
+
def default_internal_logger
|
|
184
259
|
::Logger.new($stdout).tap do |l|
|
|
185
260
|
l.level = ::Logger::WARN
|
|
186
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.2
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -41,20 +41,25 @@ 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
|
|
55
59
|
- lib/closeyourit/scope.rb
|
|
56
60
|
- lib/closeyourit/scrubber.rb
|
|
57
61
|
- lib/closeyourit/sidekiq/error_handler.rb
|
|
62
|
+
- lib/closeyourit/stats.rb
|
|
58
63
|
- lib/closeyourit/subscribers/slow_query.rb
|
|
59
64
|
- lib/closeyourit/transport.rb
|
|
60
65
|
- lib/closeyourit/version.rb
|