closeyourit-ruby 0.10.0 → 0.10.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/closeyourit/background_worker.rb +14 -14
  3. data/lib/closeyourit/breadcrumb.rb +2 -2
  4. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  5. data/lib/closeyourit/client.rb +24 -24
  6. data/lib/closeyourit/configuration.rb +102 -102
  7. data/lib/closeyourit/event.rb +6 -6
  8. data/lib/closeyourit/events/error_event.rb +16 -16
  9. data/lib/closeyourit/events/job_metric_event.rb +8 -8
  10. data/lib/closeyourit/events/log_event.rb +17 -17
  11. data/lib/closeyourit/events/message_event.rb +4 -4
  12. data/lib/closeyourit/events/performance_issue_event.rb +4 -4
  13. data/lib/closeyourit/events/slow_method_event.rb +6 -6
  14. data/lib/closeyourit/events/slow_query_event.rb +4 -4
  15. data/lib/closeyourit/instrumenter.rb +6 -6
  16. data/lib/closeyourit/line_cache.rb +4 -4
  17. data/lib/closeyourit/log_buffer.rb +10 -10
  18. data/lib/closeyourit/log_device.rb +18 -18
  19. data/lib/closeyourit/monitor.rb +2 -2
  20. data/lib/closeyourit/performance/request_profile.rb +5 -5
  21. data/lib/closeyourit/performance/rollup.rb +3 -3
  22. data/lib/closeyourit/rails/active_job_extension.rb +33 -31
  23. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  24. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  25. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  26. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  27. data/lib/closeyourit/rails/query_source.rb +3 -3
  28. data/lib/closeyourit/rails/railtie.rb +32 -52
  29. data/lib/closeyourit/rails/request_body.rb +7 -7
  30. data/lib/closeyourit/rails/request_context.rb +27 -27
  31. data/lib/closeyourit/scope.rb +29 -29
  32. data/lib/closeyourit/scrubber.rb +47 -48
  33. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  34. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
  35. data/lib/closeyourit/stats.rb +8 -8
  36. data/lib/closeyourit/subscribers/job_performance.rb +30 -19
  37. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  38. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  39. data/lib/closeyourit/trace_context.rb +22 -22
  40. data/lib/closeyourit/transport.rb +22 -22
  41. data/lib/closeyourit/usage_registry.rb +17 -16
  42. data/lib/closeyourit/version.rb +1 -1
  43. data/lib/closeyourit-ruby.rb +125 -125
  44. metadata +1 -1
@@ -4,8 +4,8 @@ require "time"
4
4
  require "socket"
5
5
 
6
6
  module CloseYourIt
7
- # Base degli eventi di telemetria. Le sottoclassi implementano `#to_h` e `#ingest_path`
8
- # (il path API a cui l'evento va spedito: errori → /events, metriche → /metrics).
7
+ # Base of telemetry events. Subclasses implement `#to_h` and `#ingest_path`
8
+ # (the API path the event is sent to: errors → /events, metrics → /metrics).
9
9
  class Event
10
10
  def initialize(configuration)
11
11
  @configuration = configuration
@@ -13,11 +13,11 @@ module CloseYourIt
13
13
  end
14
14
 
15
15
  def to_h
16
- raise NotImplementedError, "#{self.class} deve implementare #to_h"
16
+ raise NotImplementedError, "#{self.class} must implement #to_h"
17
17
  end
18
18
 
19
19
  def ingest_path(_project_id)
20
- raise NotImplementedError, "#{self.class} deve implementare #ingest_path"
20
+ raise NotImplementedError, "#{self.class} must implement #ingest_path"
21
21
  end
22
22
 
23
23
  private
@@ -42,8 +42,8 @@ module CloseYourIt
42
42
  hash.reject { |_key, value| value.nil? }
43
43
  end
44
44
 
45
- # Fusione ricorsiva: gli Hash annidati vengono fusi (es. `contexts.runtime` preservato
46
- # mentre lo scope aggiunge `contexts.active_job`), gli scalari sovrascritti.
45
+ # Recursive merge: nested Hashes are merged (e.g. `contexts.runtime` kept while the scope adds
46
+ # `contexts.active_job`), scalars are overwritten.
47
47
  def deep_merge(base, override)
48
48
  base.merge(override) do |_key, old_value, new_value|
49
49
  if old_value.is_a?(Hash) && new_value.is_a?(Hash)
@@ -7,9 +7,9 @@ require_relative "../scrubber"
7
7
  require_relative "../line_cache"
8
8
 
9
9
  module CloseYourIt
10
- # Trasforma un'eccezione Ruby nel **payload evento Sentry** che il backend CloseYourIt ingerisce
11
- # (Errors::Ingest::Normalize). Usa `backtrace_locations` (niente regex) e mette la cause-chain in
12
- # `exception.values` ordinata dall'esterna alla principale (Sentry: values.last = il crash).
10
+ # Turns a Ruby exception into the **Sentry event payload** the CloseYourIt backend ingests
11
+ # (Errors::Ingest::Normalize). Uses `backtrace_locations` (no regex) and puts the cause chain in
12
+ # `exception.values` ordered from the outermost to the main one (Sentry: values.last = the crash).
13
13
  class ErrorEvent < Event
14
14
  def self.from_exception(exception, configuration:, handled: false, level: "error", contexts: nil)
15
15
  new(exception, configuration, handled: handled, level: level, contexts: contexts)
@@ -30,7 +30,7 @@ module CloseYourIt
30
30
  "timestamp" => @occurred_at,
31
31
  "platform" => "ruby",
32
32
  "level" => @level,
33
- # Correlazione log↔errori: stesso trace_id dei log della medesima richiesta (popolato dallo Scope).
33
+ # Log↔error correlation: same trace_id as the logs of the same request (populated by the Scope).
34
34
  "trace_id" => CloseYourIt::Scope.current.trace_id,
35
35
  "environment" => environment,
36
36
  "release" => @configuration.release,
@@ -39,9 +39,9 @@ module CloseYourIt
39
39
  "contexts" => { "runtime" => { "name" => "ruby", "version" => RUBY_VERSION } },
40
40
  "sdk" => sdk
41
41
  )
42
- # Fonde il contesto per-richiesta/job (user/tags/extra/contexts/request) raccolto nello Scope.
42
+ # Merges the per-request/job context (user/tags/extra/contexts/request) collected in the Scope.
43
43
  merged = deep_merge(base, CloseYourIt::Scope.current.to_event_hash)
44
- # Context extra passato esplicitamente (es. rails_error dall'ErrorReporter).
44
+ # Extra context passed explicitly (e.g. rails_error from the ErrorReporter).
45
45
  @contexts ? deep_merge(merged, { "contexts" => @contexts }) : merged
46
46
  end
47
47
 
@@ -51,7 +51,7 @@ module CloseYourIt
51
51
 
52
52
  private
53
53
 
54
- # Cause-chain → array Sentry: causa più esterna prima, eccezione principale ULTIMA.
54
+ # Cause chain → Sentry array: outermost cause first, main exception LAST.
55
55
  def exception_values
56
56
  chain = []
57
57
  seen = []
@@ -73,10 +73,10 @@ module CloseYourIt
73
73
  }
74
74
  end
75
75
 
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.
78
- # `filename` è relativo/basename (culprit confrontabile cross-SDK — CYRB-4); `abs_path` è il
79
- # path assoluto per il dettaglio e per leggere il sorgente delle context lines.
76
+ # Sentry-style: most recent frame last; keys filename/function/lineno/in_app/abs_path
77
+ # + source snippet (pre_context/context_line/post_context) when the file is readable.
78
+ # `filename` is relative/basename (culprit comparable across SDKs — CYRB-4); `abs_path` is the
79
+ # absolute path for the detail view and for reading the context lines' source.
80
80
  def frames(locations)
81
81
  return [] if locations.nil?
82
82
 
@@ -94,15 +94,15 @@ module CloseYourIt
94
94
  end
95
95
  end
96
96
 
97
- # Path da mostrare (Sentry `filename`): relativo alla project_root se il file è dentro l'app
98
- # (es. "app/models/user.rb"), altrimenti il basename (gem/stdlib/path esterni → "base.rb").
99
- # Allinea Ruby agli altri SDK (Dart/JS mandano basename/relativo), così il culprit
100
- # "filename in function" è confrontabile a colpo d'occhio su progetti poliglotti. Vedi CYRB-4.
97
+ # Path to display (Sentry `filename`): relative to project_root if the file is inside the app
98
+ # (e.g. "app/models/user.rb"), otherwise the basename (gem/stdlib/external paths → "base.rb").
99
+ # Aligns Ruby with the other SDKs (Dart/JS send basename/relative), so the culprit
100
+ # "filename in function" is comparable at a glance on polyglot projects. See CYRB-4.
101
101
  def relative_filename(path)
102
102
  return path if path.nil?
103
103
 
104
104
  root = @configuration.project_root.to_s
105
- prefix = root.empty? ? nil : File.join(root, "") # root con separatore finale, portabile
105
+ prefix = root.empty? ? nil : File.join(root, "") # root with trailing separator, portable
106
106
  return File.basename(path) unless prefix && path.start_with?(prefix)
107
107
 
108
108
  relative = path[prefix.length..]
@@ -4,15 +4,15 @@ require "securerandom"
4
4
  require_relative "../event"
5
5
 
6
6
  module CloseYourIt
7
- # Metrica `kind=performance_issue` per un background job che ha superato una soglia di DURATA
8
- # (`subtype=slow_job`) o di ATTESA in coda (`subtype=job_queue_latency`). Spedita sulla pipeline
9
- # metriche (`/api/v1/projects/:id/metrics`), come slow_query/slow_method e i verdetti performance.
7
+ # `kind=performance_issue` metric for a background job that exceeded a DURATION threshold
8
+ # (`subtype=slow_job`) or a queue WAIT threshold (`subtype=job_queue_latency`). Sent on the metrics
9
+ # pipeline (`/api/v1/projects/:id/metrics`), like slow_query/slow_method and performance verdicts.
10
10
  #
11
- # `label` è il nome della CLASSE del job — mai gli argomenti (privacy: come slow_method invia solo
12
- # label/durata). `attempt` rende visibili i retry; `trace_id` (= job_id ActiveJob / jid Sidekiq)
13
- # correla la metrica a log ed errori dello stesso job. `duration_ms` è il valore misurato del subtype
14
- # (durata del perform per slow_job, attesa in coda per job_queue_latency) — stessa convenzione del
15
- # Rollup. I campi nil vengono omessi.
11
+ # `label` is the job CLASS name — never the arguments (privacy: like slow_method it sends only
12
+ # label/duration). `attempt` makes retries visible; `trace_id` (= ActiveJob job_id / Sidekiq jid)
13
+ # correlates the metric with logs and errors of the same job. `duration_ms` is the subtype's
14
+ # measured value (perform duration for slow_job, queue wait for job_queue_latency) — same
15
+ # convention as the Rollup. Nil fields are omitted.
16
16
  class JobMetricEvent < Event
17
17
  def initialize(attrs, configuration)
18
18
  super(configuration)
@@ -4,29 +4,29 @@ require "securerandom"
4
4
  require_relative "../scrubber"
5
5
 
6
6
  module CloseYourIt
7
- # Voce di log strutturata spedita all'ingest /logs (NON formato Sentry: i log sono uno stream con
8
- # message/level/attributes/logger). `message` E `attributes` passano dallo Scrubber (denylist +
9
- # pattern) come ErrorEvent — un log può contenere PII/segreti quanto un errore. `trace_id` è
10
- # congelato alla COSTRUZIONE (thread della richiesta) → correlazione log↔errori della stessa request
11
- # anche quando il flush avviene su un thread timer diverso.
7
+ # Structured log entry sent to the /logs ingest (NOT Sentry format: logs are a stream with
8
+ # message/level/attributes/logger). `message` AND `attributes` go through the Scrubber (denylist +
9
+ # patterns) like ErrorEvent — a log can contain as much PII/secrets as an error. `trace_id` is
10
+ # frozen at CONSTRUCTION (request thread) → log↔error correlation for the same request even when
11
+ # the flush happens on a different timer thread.
12
12
  class LogEvent < Event
13
- # Livelli canonici del backend (enum) + alias dei nomi stile ::Logger. Normalizzati QUI (fonte
14
- # unica) così ogni costruzione — via CloseYourIt.log, .logger o diretta — produce un livello valido.
15
- # L'indice in LEVELS è anche la severità numerica del livello (debug=0 … fatal=4): la STESSA mappa
16
- # di dart/js, così il gating per soglia (`logs_min_level`) filtra identico cross-SDK (CYRB-6).
13
+ # Backend canonical levels (enum) + aliases of ::Logger-style names. Normalized HERE (single
14
+ # source) so every construction — via CloseYourIt.log, .logger or directly — yields a valid level.
15
+ # The index in LEVELS is also the level's numeric severity (debug=0 … fatal=4): the SAME map as
16
+ # dart/js, so threshold gating (`logs_min_level`) filters identically across SDKs (CYRB-6).
17
17
  LEVELS = %w[debug info warning error fatal].freeze
18
18
  LEVEL_ALIASES = { "warn" => "warning", "err" => "error", "unknown" => "fatal" }.freeze
19
19
 
20
- # Normalizza qualsiasi livello (simbolo/stringa, maiuscole, alias) al nome canonico del backend.
21
- # Ignoto → "info". Metodo di classe: fonte unica riusata da `CloseYourIt.log` per il gating.
20
+ # Normalizes any level (symbol/string, uppercase, alias) to the backend canonical name.
21
+ # Unknown → "info". Class method: single source reused by `CloseYourIt.log` for gating.
22
22
  def self.normalize_level(level)
23
23
  value = level.to_s.downcase
24
24
  value = LEVEL_ALIASES.fetch(value, value)
25
25
  LEVELS.include?(value) ? value : "info"
26
26
  end
27
27
 
28
- # Severità numerica del livello (indice in LEVELS), identica alla mappa cross-SDK. Usata per il
29
- # confronto con `logs_min_level` prima di costruire/spedire il log.
28
+ # Numeric severity of the level (index in LEVELS), identical to the cross-SDK map. Used for the
29
+ # comparison with `logs_min_level` before building/sending the log.
30
30
  def self.severity(level)
31
31
  LEVELS.index(normalize_level(level))
32
32
  end
@@ -38,9 +38,9 @@ module CloseYourIt
38
38
  @attributes = attributes || {}
39
39
  @logger = logger
40
40
  @scrubber = Scrubber.new(configuration)
41
- # trace_id catturato QUI, sul thread della richiesta: il LogEvent viene bufferizzato e flushato
42
- # su un thread timer diverso, dove lo Scope corrente è di un'ALTRA richiesta (o vuoto) → leggerlo
43
- # lazy in to_h romperebbe la correlazione log↔errore. Lo congeliamo alla costruzione.
41
+ # trace_id captured HERE, on the request thread: the LogEvent is buffered and flushed on a
42
+ # different timer thread, where the current Scope belongs to ANOTHER request (or is empty) →
43
+ # reading it lazily in to_h would break log↔error correlation. We freeze it at construction.
44
44
  @trace_id = CloseYourIt::Scope.current.trace_id
45
45
  end
46
46
 
@@ -71,7 +71,7 @@ module CloseYourIt
71
71
  @scrubber.filter_params(deep_stringify_keys(@attributes))
72
72
  end
73
73
 
74
- # Chiavi sempre stringa (anche annidate): coerenza col payload JSON e con la denylist dello Scrubber.
74
+ # Keys always strings (nested too): consistent with the JSON payload and the Scrubber denylist.
75
75
  def deep_stringify_keys(value)
76
76
  case value
77
77
  when Hash then value.each_with_object({}) { |(key, val), acc| acc[key.to_s] = deep_stringify_keys(val) }
@@ -4,9 +4,9 @@ require "securerandom"
4
4
  require_relative "../scrubber"
5
5
 
6
6
  module CloseYourIt
7
- # Messaggio diagnostico esplicito (`CloseYourIt.capture_message`) nel formato evento Sentry
8
- # (`message.formatted` + level). Fonde lo Scope corrente come ErrorEvent. Il messaggio passa dallo
9
- # Scrubber (pattern) come `exception.message` di ErrorEvent — parità PII cross-evento.
7
+ # Explicit diagnostic message (`CloseYourIt.capture_message`) in Sentry event format
8
+ # (`message.formatted` + level). Merges the current Scope like ErrorEvent. The message goes through
9
+ # the Scrubber (patterns) like ErrorEvent's `exception.message` — PII parity across events.
10
10
  class MessageEvent < Event
11
11
  def initialize(message, level:, configuration:)
12
12
  super(configuration)
@@ -21,7 +21,7 @@ module CloseYourIt
21
21
  "timestamp" => @occurred_at,
22
22
  "platform" => "ruby",
23
23
  "level" => @level,
24
- # Correlazione log↔errori: stesso trace_id dei log della medesima richiesta (parità con ErrorEvent).
24
+ # Log↔error correlation: same trace_id as the logs of the same request (parity with ErrorEvent).
25
25
  "trace_id" => CloseYourIt::Scope.current.trace_id,
26
26
  "environment" => environment,
27
27
  "release" => @configuration.release,
@@ -4,10 +4,10 @@ require "securerandom"
4
4
  require_relative "../event"
5
5
 
6
6
  module CloseYourIt
7
- # Payload `kind=performance_issue` per la pipeline metriche (`/api/v1/projects/:id/metrics`).
8
- # È un VERDETTO aggregato (N+1, slow request, slow external HTTP), non una metrica grezza:
9
- # `subtype` lo qualifica, `trace_id` lo correla a log/errori della stessa richiesta. Lo SQL è già
10
- # offuscato (è il fingerprint del profilo). I campi nil vengono omessi (slow_request non ha sql).
7
+ # `kind=performance_issue` payload for the metrics pipeline (`/api/v1/projects/:id/metrics`).
8
+ # It is an aggregate VERDICT (N+1, slow request, slow external HTTP), not a raw metric:
9
+ # `subtype` qualifies it, `trace_id` correlates it with logs/errors of the same request. The SQL is
10
+ # already obfuscated (it is the profile fingerprint). Nil fields are omitted (slow_request has no sql).
11
11
  class PerformanceIssueEvent < Event
12
12
  def initialize(attrs, configuration)
13
13
  super(configuration)
@@ -5,10 +5,10 @@ require_relative "../event"
5
5
  require_relative "../scrubber"
6
6
 
7
7
  module CloseYourIt
8
- # Payload `kind=slow_method` per la pipeline metriche. Di default solo label + durata + posizione.
9
- # Gli argomenti del metodo sono inviati SOLO se `capture_method_arguments` (opt-in, default OFF):
10
- # posizionali per indice, kwargs per nome (scrub della chiave sensibile), valore via `inspect`
11
- # troncato per sicurezza JSON. Vedi PDR §9.
8
+ # `kind=slow_method` payload for the metrics pipeline. By default only label + duration + location.
9
+ # Method arguments are sent ONLY with `capture_method_arguments` (opt-in, default OFF):
10
+ # positional by index, kwargs by name (sensitive key scrubbing), value via `inspect`
11
+ # truncated for JSON safety. See PDR §9.
12
12
  class SlowMethodEvent < Event
13
13
  def initialize(label, duration_ms, location, configuration, args: nil, kwargs: nil)
14
14
  super(configuration)
@@ -41,8 +41,8 @@ module CloseYourIt
41
41
 
42
42
  private
43
43
 
44
- # Argomenti — SOLO se capture_method_arguments (opt-in). Posizionali per indice; kwargs per nome con
45
- # scrub della chiave sensibile (denylist password/token/…). Valore = inspect troncato (JSON-safe).
44
+ # Arguments — ONLY with capture_method_arguments (opt-in). Positional by index; kwargs by name with
45
+ # sensitive key scrubbing (denylist password/token/…). Value = truncated inspect (JSON-safe).
46
46
  def arguments
47
47
  return nil unless @configuration.capture_method_arguments
48
48
 
@@ -6,8 +6,8 @@ require_relative "../scrubber"
6
6
  require_relative "../scope"
7
7
 
8
8
  module CloseYourIt
9
- # Payload `kind=slow_query` per la pipeline metriche (`/api/v1/projects/:id/metrics`).
10
- # Lo SQL è offuscato (binds esclusi) — vedi PDR §9.
9
+ # `kind=slow_query` payload for the metrics pipeline (`/api/v1/projects/:id/metrics`).
10
+ # The SQL is obfuscated (binds excluded) — see PDR §9.
11
11
  class SlowQueryEvent < Event
12
12
  def initialize(payload, duration_ms, configuration)
13
13
  super(configuration)
@@ -47,8 +47,8 @@ module CloseYourIt
47
47
  connection.adapter_name.to_s.downcase
48
48
  end
49
49
 
50
- # Valori dei bind — SOLO se capture_query_bindings (opt-in, default OFF). Scrub per nome colonna
51
- # (denylist password/token/…); il valore è reso come stringa per sicurezza JSON.
50
+ # Bind values — ONLY with capture_query_bindings (opt-in, default OFF). Scrubbed by column name
51
+ # (denylist password/token/…); the value is rendered as a string for JSON safety.
52
52
  def bindings
53
53
  return nil unless @configuration.capture_query_bindings
54
54
 
@@ -3,8 +3,8 @@
3
3
  require_relative "events/slow_method_event"
4
4
 
5
5
  module CloseYourIt
6
- # Cronometra blocchi/metodi con `CLOCK_MONOTONIC` e invia un `slow_method` se la durata supera la
7
- # soglia. Gli argomenti sono inviati solo se `capture_method_arguments` (opt-in) — vedi SlowMethodEvent.
6
+ # Times blocks/methods with `CLOCK_MONOTONIC` and sends a `slow_method` when the duration exceeds
7
+ # the threshold. Arguments are sent only with `capture_method_arguments` (opt-in) — see SlowMethodEvent.
8
8
  module Instrumenter
9
9
  module_function
10
10
 
@@ -13,10 +13,10 @@ module CloseYourIt
13
13
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
14
14
  yield
15
15
  ensure
16
- # `measure` gira DENTRO il metodo dell'app (Monitor fa prepend): un guasto qui — soglia non
17
- # configurata, ingest irraggiungibile — solleverebbe in un chiamante che aveva già il suo
18
- # risultato. È l'unico ingresso della gemma che era scoperto: ora è come tutti gli altri
19
- # (CYRB-24). Un'eccezione del blocco misurato continua invece a propagare intatta.
16
+ # `measure` runs INSIDE the app's method (Monitor prepends): a failure here — threshold not
17
+ # configured, unreachable ingest — would raise in a caller that already had its result. It was
18
+ # the only unguarded entry point of the gem: now it is like all the others (CYRB-24). An
19
+ # exception from the measured block, instead, keeps propagating untouched.
20
20
  begin
21
21
  duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
22
22
  report(label, duration_ms, location, args: args, kwargs: kwargs)
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # In-process cache of source file lines, for the frames' context lines
5
+ # (pre_context/context_line/post_context). Bounded: beyond MAX_FILES it is emptied entirely —
6
+ # simple and sufficient (the files of an app in error are few and recurring). Thread-safe.
7
7
  module LineCache
8
8
  MAX_FILES = 200
9
9
 
@@ -11,7 +11,7 @@ module CloseYourIt
11
11
  @mutex = Mutex.new
12
12
 
13
13
  class << self
14
- # Righe del file (chomp-ate), oppure nil se non leggibile o path sintetico ("(eval)", "(irb)").
14
+ # The file's lines (chomped), or nil if unreadable or a synthetic path ("(eval)", "(irb)").
15
15
  def lines(path)
16
16
  return nil if path.nil? || path.empty? || path.start_with?("(")
17
17
 
@@ -3,11 +3,11 @@
3
3
  require "concurrent"
4
4
 
5
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.
6
+ # Thread-safe in-memory log buffer: collects LogEvents and flushes them in batches to /logs when
7
+ # they reach `logs_batch_size`, when `logs_flush_interval` expires (timer), or at shutdown.
8
+ # Cuts HTTP requests — logs are high-volume, unlike one-to-one errors/metrics.
9
9
  class LogBuffer
10
- attr_reader :timer # esposto per i test (verifica dell'intervallo configurato)
10
+ attr_reader :timer # exposed for tests (checks the configured interval)
11
11
 
12
12
  def initialize(client:, configuration:)
13
13
  @client = client
@@ -33,12 +33,12 @@ module CloseYourIt
33
33
 
34
34
  @client.flush_logs(batch)
35
35
  rescue StandardError => e
36
- # Il flush gira sia sul thread della richiesta (batch pieno) sia sul thread timer: un to_h /
37
- # before_send che solleva su UN evento non deve propagare nell'app né uccidere il TimerTask
38
- # (che altrimenti smette di flushare → log accumulati e persi). Ingoiato e loggato.
36
+ # The flush runs both on the request thread (full batch) and on the timer thread: a to_h /
37
+ # before_send raising on ONE event must not propagate into the app nor kill the TimerTask
38
+ # (which would otherwise stop flushing → logs pile up and are lost). Swallowed and logged.
39
39
  CloseYourIt.internal_logger.error("CloseYourIt log buffer: #{e.class}: #{e.message}")
40
- # Il batch è già stato drenato: se flush_logs solleva, quei log sono persi. Contabilizzali come
41
- # scarti per non lasciare un fallimento silenzioso (CYRB-12). `batch.to_a` è nil-safe.
40
+ # The batch has already been drained: if flush_logs raises, those logs are lost. Count them as
41
+ # drops so the failure is not silent (CYRB-12). `batch.to_a` is nil-safe.
42
42
  batch.to_a.size.times do
43
43
  CloseYourIt.stats.increment(:dropped)
44
44
  CloseYourIt.notify_diagnostic(:drop, reason: :error)
@@ -60,7 +60,7 @@ module CloseYourIt
60
60
  end
61
61
  end
62
62
 
63
- # Avvia il timer di flush periodico alla prima voce (una sola volta).
63
+ # Starts the periodic flush timer on the first entry (only once).
64
64
  def ensure_timer
65
65
  return if @timer
66
66
 
@@ -1,28 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- # Oggetto Logger-compatibile esposto come `CloseYourIt.logger`: ogni chiamata inoltra a
5
- # `CloseYourIt.emit_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.
4
+ # Logger-compatible object exposed as `CloseYourIt.logger`: every call forwards to
5
+ # `CloseYourIt.emit_log` (→ ingest /logs). Usable as the app's explicit logger, also with attributes:
6
+ # CloseYourIt.logger.warn("disk almost full", disk: "sda1")
7
+ # `warn` maps to the `warning` level (backend enum); supports blocks and `::Logger#add` as a drop-in.
8
8
  #
9
- # La sorgente del log (campo `logger` del payload) si imposta SOLO via `.named` (child logger, parità
10
- # dart/js) o via costruttore — MAI da una keyword dei metodi: così una chiave `logger` passata a un
11
- # metodo resta un attributo dati e non viene hijackata come sorgente (CYRB-8).
12
- # payments = CloseYourIt.logger.named("payments") # child con sorgente "payments"
9
+ # The log source (the payload's `logger` field) is set ONLY via `.named` (child logger, dart/js
10
+ # parity) or the constructor — NEVER from a method keyword: so a `logger` key passed to a method
11
+ # stays a data attribute and is not hijacked as the source (CYRB-8).
12
+ # payments = CloseYourIt.logger.named("payments") # child with source "payments"
13
13
  # payments.warn("retry", attempt: 3) # logger=payments, attributes={attempt:3}
14
- # CloseYourIt.logger.warn("disco pieno", logger: "/dev/sda1") # attributes={logger:"/dev/sda1"}
14
+ # CloseYourIt.logger.warn("disk full", logger: "/dev/sda1") # attributes={logger:"/dev/sda1"}
15
15
  class LogDevice
16
- # Severità numeriche ::Logger → livelli CloseYourIt (UNKNOWN→fatal).
16
+ # ::Logger numeric severities → CloseYourIt levels (UNKNOWN→fatal).
17
17
  SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
18
18
 
19
- # `source` = nome della sorgente del log (campo `logger` del payload). Preferire `.named` in app.
19
+ # `source` = name of the log source (the payload's `logger` field). Prefer `.named` in apps.
20
20
  def initialize(source = nil)
21
21
  @source = normalize_source(source)
22
22
  end
23
23
 
24
- # Child logger con sorgente esplicita (parità dart/js). Ritorna un NUOVO LogDevice: il logger su
25
- # cui è chiamato resta invariato (immutabile), come i child logger cross-SDK.
24
+ # Child logger with an explicit source (dart/js parity). Returns a NEW LogDevice: the logger it is
25
+ # called on stays unchanged (immutable), like the cross-SDK child loggers.
26
26
  # CloseYourIt.logger.named("payments").info("retry", attempt: 3)
27
27
  def named(source)
28
28
  self.class.new(source)
@@ -39,7 +39,7 @@ module CloseYourIt
39
39
  message
40
40
  end
41
41
 
42
- # Compat con ::Logger#add(severity, message = nil, progname = nil).
42
+ # Compatible with ::Logger#add(severity, message = nil, progname = nil).
43
43
  def add(severity, message = nil, progname = nil, &block)
44
44
  write(SEVERITY_LEVELS.fetch(severity.to_i, "info"), message || progname, {}, &block)
45
45
  end
@@ -48,16 +48,16 @@ module CloseYourIt
48
48
  private
49
49
 
50
50
  def write(level, message, attributes, &block)
51
- # Gate PRIMA del block: `logger.debug { dump_costoso }` non valuta il block se i log sono spenti.
51
+ # Gate BEFORE the block: `logger.debug { expensive_dump }` does not evaluate the block when logs are off.
52
52
  return unless CloseYourIt.logs_active?
53
53
 
54
54
  message = block.call if block
55
- # Sorgente e attributes separati: gli `attributes` (inclusa un'eventuale chiave `logger`) restano
56
- # dati; la sorgente è SOLO `@source` (via `.named`/costruttore) → nessuna collisione (CYRB-8).
55
+ # Source and attributes kept apart: the `attributes` (including any `logger` key) stay data;
56
+ # the source is ONLY `@source` (via `.named`/constructor) → no collision (CYRB-8).
57
57
  CloseYourIt.emit_log(level, message, source: @source, attributes: attributes)
58
58
  end
59
59
 
60
- # Sorgente vuota/blank → nessuna sorgente (nil); altrimenti stringa (il campo `logger` è testuale).
60
+ # Empty/blank source → no source (nil); otherwise a string (the `logger` field is textual).
61
61
  def normalize_source(source)
62
62
  return nil if source.nil?
63
63
 
@@ -3,7 +3,7 @@
3
3
  require_relative "instrumenter"
4
4
 
5
5
  module CloseYourIt
6
- # Macro per strumentare automaticamente un metodo:
6
+ # Macro to instrument a method automatically:
7
7
  #
8
8
  # class Report
9
9
  # include CloseYourIt::Monitor
@@ -11,7 +11,7 @@ module CloseYourIt
11
11
  # monitor :generate
12
12
  # end
13
13
  #
14
- # Wrappa il metodo via `Module#prepend` cronometrandolo, senza cambiarne firma/risultato.
14
+ # Wraps the method via `Module#prepend` timing it, without changing its signature/result.
15
15
  module Monitor
16
16
  def self.included(base)
17
17
  base.extend(ClassMethods)
@@ -2,11 +2,11 @@
2
2
 
3
3
  module CloseYourIt
4
4
  module Performance
5
- # Accumulatore per-richiesta (vive nello Scope, resettato a fine richiesta). Conta le query, le
6
- # raggruppa per [fingerprint SQL offuscato, call-site] (il pattern prosopite per l'N+1) e tiene le
7
- # chiamate HTTP esterne. Puro stato in memoria: il verdetto lo calcola Performance::Rollup.
5
+ # Per-request accumulator (lives in the Scope, reset at the end of the request). Counts queries,
6
+ # groups them by [obfuscated SQL fingerprint, call site] (the prosopite pattern for N+1) and keeps
7
+ # the external HTTP calls. Pure in-memory state: Performance::Rollup computes the verdict.
8
8
  class RequestProfile
9
- # Guard di memoria: cap ai gruppi/chiamate distinti tracciati (il conteggio totale resta esatto).
9
+ # Memory guard: cap on the distinct groups/calls tracked (the total count stays exact).
10
10
  MAX_GROUPS = 1000
11
11
  MAX_EXTERNAL = 500
12
12
 
@@ -19,7 +19,7 @@ module CloseYourIt
19
19
  @external_calls = []
20
20
  end
21
21
 
22
- # Una query non di sistema. Le query da cache non sono round-trip DB → non contano per l'N+1.
22
+ # A non-system query. Cached queries are not DB round-trips → they do not count for N+1.
23
23
  def add_query(fingerprint:, source:, duration_ms:, cached: false)
24
24
  return if cached
25
25
 
@@ -4,8 +4,8 @@ require_relative "../events/performance_issue_event"
4
4
 
5
5
  module CloseYourIt
6
6
  module Performance
7
- # Trasforma un RequestProfile (+ durata/route della richiesta) in 0..N verdetti PerformanceIssueEvent.
8
- # Le soglie vivono nella Configuration. Detection lato client; dedup/alert lato backend.
7
+ # Turns a RequestProfile (+ request duration/route) into 0..N PerformanceIssueEvent verdicts.
8
+ # Thresholds live in the Configuration. Detection on the client; dedup/alerts on the backend.
9
9
  class Rollup
10
10
  def self.call(...) = new(...).call
11
11
 
@@ -28,7 +28,7 @@ module CloseYourIt
28
28
 
29
29
  private
30
30
 
31
- # Un verdetto per ogni gruppo [fingerprint, call-site] che ha girato più di n_plus_one_threshold volte.
31
+ # One verdict for every [fingerprint, call-site] group that ran more than n_plus_one_threshold times.
32
32
  def n_plus_one_events
33
33
  @profile.query_groups.values.filter_map do |group|
34
34
  next unless group[:count] > @config.n_plus_one_threshold