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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b015462d6b0e563ef202410b5f2bc33b11ee10f985d984ac84a874836f67c82e
4
- data.tar.gz: 41f3e3ca878dbfbfd818cd4963b238d1d78400344e0e8bbb95cfd025853ebf82
3
+ metadata.gz: 0e319cd48ab53d0933f14344dc8ea2ac503ed7537b8111789fd540cf6bc3d65f
4
+ data.tar.gz: 2f7d6223045d8c9aea2ad4d1b9b6bac4cd1420cbc8abe2bd91622855cb45dfae
5
5
  SHA512:
6
- metadata.gz: 16d70f8ef7436397d881a8f67664f24aaabfe3ae976d5b23276149242bf8f539d1c5dc1d27f9d7c8a2c72a080ab42b2b56c11ec8af1e0fae108e06f5bf8cfbdc
7
- data.tar.gz: '02853ab18aad59772dd4aa0cc497c32983e4e49f2926d2b42d28654552dabad1c0f9dacd589fc0423c920cbb1ea903457a35767365130777dc4d0317282e7225'
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.logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
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)
@@ -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://). Chiamata da `CloseYourIt.init`.
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.logger.warn(insecure_endpoint_message) if insecure_endpoint?
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
- CloseYourIt::Scope.current.request = build_request(env) if active?
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 active?
27
- CloseYourIt.enabled? && CloseYourIt.configuration.capture_request
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)
@@ -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 da solo. 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.
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.logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
31
+ CloseYourIt.stats.increment(:failed)
32
+ CloseYourIt.internal_logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
25
33
  nil
26
34
  end
27
35
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.2"
5
5
  end
@@ -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
- @logger ||= default_logger
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
- attr_writer :logger
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 default_logger
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.1
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